From f44350aab5e44f1689bfea346092ff95d7748a75 Mon Sep 17 00:00:00 2001 From: vinid Date: Sun, 7 Jul 2024 12:12:29 -0400 Subject: [PATCH] support for png and jpgs without having to swap encodings --- README.md | 18 +- .../notebooks/Local-Model-With-LMStudio.ipynb | 6 +- .../Tutorial-MultiModal-DeepDive.ipynb | 388 ++++++++++++++++++ ...Vision.ipynb => Tutorial-MultiModal.ipynb} | 151 +++++-- ...itives.ipynb => Tutorial-Primitives.ipynb} | 90 +++- ...ynb => Tutorial-Prompt-Optimization.ipynb} | 0 requirements.txt | 3 +- setup.py | 4 +- textgrad/autograd/multimodal_ops.py | 22 + textgrad/engine/__init__.py | 20 +- textgrad/engine/anthropic.py | 5 +- textgrad/engine/openai.py | 4 +- 12 files changed, 638 insertions(+), 73 deletions(-) create mode 100644 examples/notebooks/Tutorial-MultiModal-DeepDive.ipynb rename examples/notebooks/{TextGrad-Vision.ipynb => Tutorial-MultiModal.ipynb} (99%) rename examples/notebooks/{Primitives.ipynb => Tutorial-Primitives.ipynb} (87%) rename examples/notebooks/{Prompt-Optimization.ipynb => Tutorial-Prompt-Optimization.ipynb} (100%) diff --git a/README.md b/README.md index 9f4251a..1c832a9 100644 --- a/README.md +++ b/README.md @@ -103,17 +103,19 @@ We have many more examples around how TextGrad can optimize all kinds of variabl ### Tutorials -We have prepared a couple of tutorials to get you started with TextGrad. -You can run them directly in Google Colab by clicking on the links below. +We have prepared a couple of tutorials to get you started with TextGrad. The order of this +tutorial is what we would recommend to follow for a beginner. You can run them directly in Google Colab by clicking on the links below (but +you need an OpenAI/Anthropic key to run the LLMs).
-| Example | Colab Link | -|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Introduction to TextGrad Primitives | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Primitives.ipynb) | -| Optimizing a Code Snippet and Define a New Loss | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/textgrad/blob/main/examples/notebooks/Tutorial-Test-Time-Loss-for-Code.ipynb) | -| Prompt Optimization | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Prompt-Optimization.ipynb) | -| Solution Optimization | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-Solution-Optimization.ipynb) | +| Tutorial | Difficulty | Colab Link | +|----------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1. Introduction to TextGrad Primitives | ![](https://img.shields.io/badge/Level-Beginner-green.svg) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-Primitives.ipynb) | +| 2. Solution Optimization | ![](https://img.shields.io/badge/Level-Beginner-green.svg) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-Solution-Optimization.ipynb) | +| 3. Optimizing a Code Snippet and Define a New Loss | ![](https://img.shields.io/badge/Level-Beginner-green.svg) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/textgrad/blob/main/examples/notebooks/Tutorial-Test-Time-Loss-for-Code.ipynb) | +| 4. Prompt Optimization | ![](https://img.shields.io/badge/Level-Intermediate-yellow.svg) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-Prompt-Optimization.ipynb) | +| 5. MultiModal Optimization | ![](https://img.shields.io/badge/Level-Beginner-green.svg) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Tutorial-MultiModal.ipynb) |
diff --git a/examples/notebooks/Local-Model-With-LMStudio.ipynb b/examples/notebooks/Local-Model-With-LMStudio.ipynb index 977f148..0fdca0d 100644 --- a/examples/notebooks/Local-Model-With-LMStudio.ipynb +++ b/examples/notebooks/Local-Model-With-LMStudio.ipynb @@ -182,7 +182,7 @@ ], "metadata": { "kernelspec": { - "display_name": "textgrad", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -196,9 +196,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/notebooks/Tutorial-MultiModal-DeepDive.ipynb b/examples/notebooks/Tutorial-MultiModal-DeepDive.ipynb new file mode 100644 index 0000000..a3aa5e1 --- /dev/null +++ b/examples/notebooks/Tutorial-MultiModal-DeepDive.ipynb @@ -0,0 +1,388 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "26705673-b9e8-4d6b-b5b6-a1cf47d1df4d", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } + }, + "source": [ + "# TextGrad Tutorials: MultiModal Optimization\n", + "\n", + "![TextGrad](https://github.com/vinid/data/blob/master/logo_full.png?raw=true)\n", + "\n", + "An autograd engine -- for textual gradients!\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Prompt-Optimization.ipynb)\n", + "[![GitHub license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)\n", + "[![Arxiv](https://img.shields.io/badge/arXiv-2406.07496-B31B1B.svg)](https://arxiv.org/abs/2406.07496)\n", + "[![Documentation Status](https://readthedocs.org/projects/textgrad/badge/?version=latest)](https://textgrad.readthedocs.io/en/latest/?badge=latest)\n", + "[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/textgrad)](https://pypi.org/project/textgrad/)\n", + "[![PyPI](https://img.shields.io/pypi/v/textgrad)](https://pypi.org/project/textgrad/)\n", + "\n", + "**Objectives for this tutorial:**\n", + "\n", + "* Explore some more MultiModal cases in TextGrad. Using a dataset from the literature.\n", + "\n", + "**Requirements:**\n", + "\n", + "* You need to have an OpenAI API key to run this tutorial. This should be set as an environment variable as OPENAI_API_KEY.\n" + ] + }, + { + "cell_type": "markdown", + "id": "f10aa9d1-8482-4db7-97af-fa68782e5a4a", + "metadata": {}, + "source": [ + "## Image Support in TextGrad\n", + "\n", + "We currently supports PNG and JPEG images. We have a few examples below to show how to use images in TextGrad. If your image is in a different format you should convert it. Here is an example function that \n", + "does that for you. \n", + "\n", + "The way we support images is through the byte format. This is then converted to a Base64 string and sent to the OpenAI/Anthropic API." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "db295b99-e94d-44a9-b904-b1aa9cbb7888", + "metadata": {}, + "outputs": [], + "source": [ + "# Some utils to read images\n", + "\n", + "import io\n", + "from PIL import Image\n", + "\n", + "# \n", + "def encode_image(image):\n", + " # Convert RGBA to RGB if necessary\n", + " if image.mode == 'RGBA':\n", + " # Create a new image with a white background\n", + " background = Image.new('RGB', image.size, (255, 255, 255))\n", + " # Paste the image on the background.\n", + " background.paste(image, (0, 0), image)\n", + " image = background\n", + "\n", + " # Create a BytesIO object\n", + " buffered = io.BytesIO()\n", + "\n", + " # Save your image object to this BytesIO object (in JPEG format)\n", + " image.save(buffered, format=\"JPEG\")\n", + "\n", + " # Get the byte data from the BytesIO object\n", + " image_byte_data = buffered.getvalue()\n", + " return image_byte_data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "24cd0a8e-6b32-4cea-9e0a-3d95260eea49", + "metadata": { + "ExecuteTime": { + "end_time": "2024-07-07T15:52:16.587781196Z", + "start_time": "2024-07-07T15:52:16.174639147Z" + } + }, + "outputs": [], + "source": [ + "import textgrad as tg\n", + "\n", + "# differently from the past tutorials, we now need a multimodal LLM call instead of a standard one!\n", + "from textgrad.autograd import MultimodalLLMCall\n", + "from textgrad.loss import ImageQALoss\n", + "from datasets import load_dataset\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "89c990a4-4784-4c25-9374-c76552d7f974", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dotenv import load_dotenv\n", + "load_dotenv(\".env\", override=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "403c37ff-618c-4b64-a7fd-bf1f514c79b5", + "metadata": {}, + "outputs": [], + "source": [ + "tg.set_backward_engine(\"gpt-4o\", override=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "d7e06dda-e86f-4ff1-acb6-625934bb54f5", + "metadata": {}, + "outputs": [], + "source": [ + "ds = load_dataset(\"derek-thomas/ScienceQA\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d226b34-167c-4a31-8649-a9e4a026d257", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "cb10a125-ab04-40c1-9875-9876a3d7cc11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Which solution has a higher concentration of blue particles?\n", + "\n", + "-neither; their concentrations are the same\n", + "-Solution B\n", + "-Solution A\n" + ] + } + ], + "source": [ + "target_image = ds[\"train\"][10][\"image\"]\n", + "target_question = ds[\"train\"][\"question\"][10]\n", + "target_options = ds[\"train\"][\"choices\"][10]\n", + "target_options = \"\\n-\".join(target_options)\n", + "target_correct_answer = ds[\"train\"][\"answer\"][10]\n", + "\n", + "question_for_model = f\"{target_question}\\n\\n-{target_options}\"\n", + "print(question_for_model)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "839654b8-91da-43f7-ba61-b9bff9799d07", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "image/png": "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6upbW6aFxcvBPIZBvHo3atQ8a5X8vRue9Jy1iiECsa9yB5YU3inEuyEj8FNCUeZSwiKcUat09g9v7e4neXUocM5BC2ebtyjl2nYrUAM7MA6Z3+8vneGlAwH3S65DorHdInH2eM0plAIip25zvewpx2Hp6UZR8cjqTQ+t3MgEPhzW1dY11tdHWIMqyATfbuvJi3dTaK2itY1WOzg9cJbZCq5V1HLCMrocR0m9G+YDRVG0kLv6hw0gfHu3Nb5uz9n11bk9Z70fux9PAxaHiljloOxe9iTgcKLoBvP3DMYeWrlh0cYOO9/leA/D6PiFLfVHzG+T95jsViINWBx6UOQE4XAC6ca9SaatXt3es7l3YHMP7yWOOx7kpt+pAjSapkUjrbXVsxtr953exOFOdfuTebMWh1m0PTk4nAy6yXKT9pTVgNXAzqMBW5fuPLa2d2o1MMUasHQzxQawl7ca2Hk0YOlm57G1vVOrgSnWgKWbKTaAvbzVwM6jAUs3O4+t7Z1aDUyxBizdTLEB7OWtBnYeDVi62Xlsbe/UamCKNWDpZooNYC9vNbDzaMDSzc5ja3unVgNTrAFLN1NsAHt5q4GdRwOWbnYeW9s7tRqYYg1YupliA9jLWw3sPBqwdLPz2NreqdXAFGvA0s0UG8Be3mpg59GApZudx9b2Tq0GplgDlm6m2AD28lYDO48GLN3sPLa2d2o1MMUasHQzxQawl7ca2Hk0IHTT3d39zW9+88QTT3zFK16x3377nXzyyTfddFMslnvVvosuuugd73hHmSurHArZ19f3la985fjjj997771f85rXfPe739VVDVEdOt91++3OO+/0rNLFixcj7L777vMsYQozWhxOtPKnHIch7vC8885bu3btJZdcsu+++8Iy//rXv2AfYnhCJvr+x5N/6KGH/uEPf5g7d+54CSor/tJLL/33v//9yU9+cpdddnnssce++tWvoucPfehDLIAHAi6++OKjjz7avaPdd9/dDe9UAYvDiTb3lOMw9Pzzzz/55JM333zzKaecond72GGHVVdX33vvvf39/TU1znp/E62IdPnr1q3btm1bekxFhzs6Oh588MHPfe5zZ5xxBjdyxBFHLFq06J577oFuent7idl///2POuqoir7H4gtvcVi8DrNLKAccBuJxWQ+b5bzTy3rhhRfefvvtLtfcdtttr3vd6/bc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", + "text/plain": [ + "" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "target_image" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "122e4cf1-cd3f-4818-aefa-a706d7c9ea83", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7595d976-ed94-4499-9735-02dbb20d3291", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "e00c0fef-d1d7-4b2f-8ea9-5547124cc775", + "metadata": {}, + "outputs": [], + "source": [ + "target_image = encode_image(target_image)\n", + "\n", + "image_variable = tg.Variable(target_image, role_description=\"image to answer a question about\", requires_grad=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "4b8dbf50-fb97-4432-b544-0092ffd1b187", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Variable(value=Solution B has a higher concentration of blue particles. Both solutions have the same solvent volume (35 mL), but Solution B contains more blue particles than Solution A., role=response from the language model, grads=set())" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question_variable = tg.Variable(question_for_model, role_description=\"question to answer\", requires_grad=False)\n", + "response = MultimodalLLMCall(\"gpt-4o\")([image_variable, question_variable])\n", + "response" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "d875d5b5-331e-4870-99aa-5eb33667d7da", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Variable(value=The existing answer correctly identifies that Solution B has a higher concentration of blue particles. The reasoning provided is accurate: both solutions have the same solvent volume (35 mL), but Solution B contains more blue particles than Solution A. This indicates a higher concentration of blue particles in Solution B. The answer accurately understands the image and provides appropriate knowledge and reasoning logic to address the question., role=evaluation of the response from the language model, grads=set())" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loss_fn = ImageQALoss(\n", + " evaluation_instruction=\"Please evaluate the existing answer to the visual scientific problem without solving it yourself. Verify that the answer accurately understands the image, provides appropriate knowledge and reasoning logic to address the question.\",\n", + " engine=\"gpt-4o\"\n", + ")\n", + "loss = loss_fn(question=question_variable, image=image_variable, response=response)\n", + "loss" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb26a96c-a229-4a99-8c31-49de8aabdf40", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3c5b32e-92fd-40a8-a35b-3d78fcfc313a", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8a96941-17b5-49b0-9c3b-e5d9fd6bf229", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "85befc59-bb38-463a-a70c-9e005e447689", + "metadata": {}, + "source": [ + "### Direct PNG" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "34af1d5d-ed95-4676-83d9-9ae27de7d221", + "metadata": {}, + "outputs": [], + "source": [ + "import httpx\n", + "\n", + "image_url = \"https://d2bzx2vuetkzse.cloudfront.net/fit-in/0x450/images_without_background/45ca6024-4bf0-43b8-9a3a-b4a44ecac0bf.png\"\n", + "image_data = httpx.get(image_url).content" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "c65f1f1b-36b0-47ed-8b28-61fa9269148d", + "metadata": {}, + "outputs": [], + "source": [ + "image_variable = tg.Variable(image_data, role_description=\"image to answer a question about\", requires_grad=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "ca6a613f-ce72-4e58-b009-49e41af1763a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Variable(value=The image shows a small, brown rodent that appears to be a capybara. Capybaras are the largest rodents in the world and are native to South America. They have a distinctive appearance with a large, barrel-shaped body, short legs, and a blunt snout. This particular capybara is sitting and facing to the right., role=response from the language model, grads=set())" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "question_variable = tg.Variable(\"What do you see in this image?\", role_description=\"question\", requires_grad=False)\n", + "response = MultimodalLLMCall(\"gpt-4o\")([image_variable, question_variable])\n", + "response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3218180-eb0f-47d9-a3b7-309f1e994144", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/TextGrad-Vision.ipynb b/examples/notebooks/Tutorial-MultiModal.ipynb similarity index 99% rename from examples/notebooks/TextGrad-Vision.ipynb rename to examples/notebooks/Tutorial-MultiModal.ipynb index 8f8fa62..c011294 100644 --- a/examples/notebooks/TextGrad-Vision.ipynb +++ b/examples/notebooks/Tutorial-MultiModal.ipynb @@ -1,20 +1,35 @@ { "cells": [ { - "cell_type": "code", - "execution_count": 1, - "id": "6e5bfa16-5124-452c-bc56-3427e453751a", + "cell_type": "markdown", + "id": "0023a2ae-72fe-490b-b715-4dddb2539c38", "metadata": {}, - "outputs": [], "source": [ - "%load_ext autoreload\n", + "# TextGrad Tutorials: MultiModal Optimization\n", + "\n", + "![TextGrad](https://github.com/vinid/data/blob/master/logo_full.png?raw=true)\n", + "\n", + "An autograd engine -- for textual gradients!\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/zou-group/TextGrad/blob/main/examples/notebooks/Prompt-Optimization.ipynb)\n", + "[![GitHub license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)\n", + "[![Arxiv](https://img.shields.io/badge/arXiv-2406.07496-B31B1B.svg)](https://arxiv.org/abs/2406.07496)\n", + "[![Documentation Status](https://readthedocs.org/projects/textgrad/badge/?version=latest)](https://textgrad.readthedocs.io/en/latest/?badge=latest)\n", + "[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/textgrad)](https://pypi.org/project/textgrad/)\n", + "[![PyPI](https://img.shields.io/pypi/v/textgrad)](https://pypi.org/project/textgrad/)\n", + "\n", + "**Objectives for this tutorial:**\n", + "\n", + "* Introduce you to multimodal optimization with TextGrad\n", "\n", - "%autoreload 2" + "**Requirements:**\n", + "\n", + "* You need to have an OpenAI API key to run this tutorial. This should be set as an environment variable as OPENAI_API_KEY.\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "8dd3140c-45d0-478e-b184-ec5faed66964", "metadata": {}, "outputs": [], @@ -48,26 +63,48 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 2, "id": "3ca4c62b-2d83-412d-b410-0ed5272a6f06", "metadata": {}, "outputs": [], "source": [ "import textgrad as tg\n", + "\n", + "# differently from the past tutorials, we now need a multimodal LLM call instead of a standard one!\n", "from textgrad.autograd import MultimodalLLMCall\n", "from textgrad.loss import ImageQALoss" ] }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 6, + "id": "2b06474c-491d-48ff-aef1-62cb0e525473", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dotenv import load_dotenv\n", + "load_dotenv(\".env\", override=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, "id": "d1986a01-3afd-46a8-a99e-e977b1141768", "metadata": {}, "outputs": [], "source": [ - "from dotenv import load_dotenv\n", - "load_dotenv(override=True)\n", - "tg.set_backward_engine(\"claude-3-haiku-20240307\")" + "tg.set_backward_engine(\"gpt-4o\")" ] }, { @@ -78,29 +115,71 @@ "# Simply answering questions about images" ] }, + { + "cell_type": "markdown", + "id": "efa853c2-2703-4304-a9c5-a3bde675b532", + "metadata": {}, + "source": [ + "We now downlaod an image from the web." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "950b3502-97ce-4581-87c7-4c47421beafc", + "metadata": {}, + "outputs": [], + "source": [ + "import httpx\n", + "\n", + "image_url = \"https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg\"\n", + "image_data = httpx.get(image_url).content" + ] + }, + { + "cell_type": "markdown", + "id": "1ad925fa-c1e6-482d-af4c-df6f8dcb2c2f", + "metadata": {}, + "source": [ + "As usual, in TextGrad we now have to transform our object of interest into a Variable object. In the previous tutorials, we were doing this with text data, now we are going to do this with Images." + ] + }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 11, "id": "e0629de4-9fcf-4df4-9316-cc86455929e6", "metadata": {}, + "outputs": [], + "source": [ + "image_variable = tg.Variable(image_data, role_description=\"image to answer a question about\", requires_grad=False)" + ] + }, + { + "cell_type": "markdown", + "id": "1fcbfaaf-8aa4-4bfa-82af-bf5b7aef0f94", + "metadata": {}, + "source": [ + "Let's now ask as question!" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1d940cf3-a461-43f4-bc4a-6103589b159e", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Variable(value=This image shows a close-up of an ant. The ant appears to be black and is standing on a surface, possibly the ground. The image is highly detailed, showing the ant's body segments, legs, and antennae. The background is blurred, which helps to focus attention on the ant., role=response from the language model, grads=set())" + "Variable(value=This image shows a close-up of an ant. The ant appears to be black and is standing on a surface, possibly a ground or a floor. The image is highly detailed, showing the ant's body segments, legs, and antennae. The background is blurred, which helps to focus attention on the ant., role=response from the language model, grads=set())" ] }, - "execution_count": 89, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import httpx\n", - "\n", - "image_url = \"https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg\"\n", - "image_data = httpx.get(image_url).content\n", - "image_variable = tg.Variable(image_data, role_description=\"image to answer a question about\", requires_grad=False)\n", "question_variable = tg.Variable(\"What do you see in this image?\", role_description=\"question\", requires_grad=False)\n", "response = MultimodalLLMCall(\"gpt-4o\")([image_variable, question_variable])\n", "response" @@ -131,39 +210,37 @@ }, { "cell_type": "code", - "execution_count": 90, + "execution_count": 15, "id": "29affc0a-cedc-40fd-bec4-6bf5178409cf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Variable(value=This answer, while providing some accurate information, falls short of being a complete and good response for the image. Let's critically evaluate it:\n", - "\n", - "1. Incompleteness: The answer fails to mention several key details visible in the image. For instance, it doesn't describe the ant's posture (reared up on its hind legs), the texture of its exoskeleton, or the fine hairs visible on its body.\n", + "Variable(value=The provided answer is quite detailed and covers many aspects of the image. However, there are a few points that could be improved or clarified:\n", "\n", - "2. Lack of precision: The description of the ant as \"black\" is imprecise. The ant appears to have a dark, metallic sheen that could be better described as gunmetal or dark gray.\n", + "1. **Species Identification**: The answer mentions \"likely a species of black ant,\" which is a bit vague. While it's understandable that the exact species might not be identifiable, it could be better to simply state that it is a black ant without speculating on the species.\n", "\n", - "3. Missing context: The answer doesn't comment on the exceptional quality of the macro photography, which is a significant aspect of this image.\n", + "2. **Surface Description**: The answer states the ant is on a \"textured surface, possibly concrete or soil.\" This is a reasonable guess, but it could be more concise by just mentioning a textured surface without speculating on the material.\n", "\n", - "4. Overlooked details: The response fails to mention the ant's mandibles, which are clearly visible and an important feature of the image.\n", + "3. **Ant's Posture**: The description of the ant's posture as \"alert or defensive\" is speculative. While the ant's posture is indeed notable, it might be better to describe it without attributing a specific behavior unless it is clearly evident.\n", "\n", - "5. Lack of depth: There's no attempt to describe the ant's behavior or posture, which appears to be in an alert or defensive stance.\n", + "4. **Background Description**: The explanation of the background being blurred due to a shallow depth of field is accurate and well-explained.\n", "\n", - "6. Vague background description: While the answer mentions a blurred background, it doesn't describe the colors visible (reddish and green tones), which contribute to the overall composition.\n", + "5. **Detail Description**: The mention of the texture of the ant's body, the shine on its exoskeleton, and the fine hairs on its legs is excellent and adds to the vividness of the description.\n", "\n", - "7. Surface description: The answer is uncertain about the surface the ant is on, when it's clearly a textured, light-colore, role=evaluation of the response from the language model, grads=set())" + "Overall, the answer is comprehensive and well-articulated but could benefit from slightly less speculation and more straightforward descriptions., role=evaluation of the response from the language model, grads=set())" ] }, - "execution_count": 90, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "loss_fn = ImageQALoss(\n", - " evaluation_instruction=\"Does this seem like a complete and good answer for the image? Criticize.\",\n", - " engine=\"claude-3-5-sonnet-20240620\"\n", + " evaluation_instruction=\"Does this seem like a complete and good answer for the image? Criticize. Do not provide a new answer.\",\n", + " engine=\"gpt-4o\"\n", ")\n", "loss = loss_fn(question=question_variable, image=image_variable, response=response)\n", "loss" @@ -171,7 +248,7 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": 16, "id": "38c2d4ff-1458-459d-8915-3d1a254564fb", "metadata": {}, "outputs": [ @@ -179,9 +256,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "This image showcases an exceptional close-up view of a metallic, gunmetal-gray ant standing alert on a textured, light-colored surface. The ant's exoskeleton has a striking, almost iridescent sheen, and its body is covered in fine hairs that are clearly visible. The ant's prominent mandibles are positioned in a defensive stance, with the creature reared up on its hind legs, conveying a sense of vigilance and aggression.\n", - "\n", - "The exceptional macro-level detail and focus of the photography isolates the ant and draws the viewer's attention to its intricate features. The blurred, reddish and green-toned background further emphasizes the ant, creating a dramatic, almost cinematic quality to the image. This close-up perspective provides a rare glimpse into the biology and behavior of this small but fascinating creature, revealing insights into its role within the broader natural world.\n" + "This image shows a close-up of a black ant, captured using macro photography. The ant is standing on a textured surface. The image is highly detailed, showcasing the ant's body segments, legs, and antennae with great clarity. The ant's head is raised, and its antennae are extended. The background is blurred, employing a shallow depth of field to focus attention on the ant and highlight its intricate details. The texture of the ant's body segments, the shine on its exoskeleton, and the fine hairs on its legs are all clearly visible, adding to the vividness of the image.\n" ] } ], @@ -225,7 +300,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/examples/notebooks/Primitives.ipynb b/examples/notebooks/Tutorial-Primitives.ipynb similarity index 87% rename from examples/notebooks/Primitives.ipynb rename to examples/notebooks/Tutorial-Primitives.ipynb index 46b6c6f..6ffb8f5 100644 --- a/examples/notebooks/Primitives.ipynb +++ b/examples/notebooks/Tutorial-Primitives.ipynb @@ -64,7 +64,10 @@ "cell_type": "markdown", "id": "8887fbed36c7daf2", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Introduction: Variable\n", @@ -89,7 +92,10 @@ "end_time": "2024-06-11T15:43:17.669096228Z", "start_time": "2024-06-11T15:43:17.665325560Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -105,7 +111,10 @@ "end_time": "2024-06-11T15:43:18.184004948Z", "start_time": "2024-06-11T15:43:18.178187640Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -127,7 +136,10 @@ "cell_type": "markdown", "id": "63f6a6921a1cce6a", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Introduction: Engine\n", @@ -144,7 +156,10 @@ "end_time": "2024-06-11T15:44:32.606319032Z", "start_time": "2024-06-11T15:44:32.561460448Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -155,7 +170,10 @@ "cell_type": "markdown", "id": "33c7d6eaa115cd6a", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "This object behaves like you would expect an LLM to behave: You can sample generation from the engine using the `generate` method. " @@ -170,7 +188,10 @@ "end_time": "2024-06-11T17:29:41.108552705Z", "start_time": "2024-06-11T17:29:40.294256814Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -192,7 +213,10 @@ "cell_type": "markdown", "id": "b627edc07c0d3737", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Introduction: Loss\n", @@ -209,7 +233,10 @@ "end_time": "2024-06-11T15:44:32.894722136Z", "start_time": "2024-06-11T15:44:32.890708561Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -221,7 +248,10 @@ "cell_type": "markdown", "id": "ff137c99e0659dcc", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [] }, @@ -229,7 +259,10 @@ "cell_type": "markdown", "id": "6f05ec2bf907b3ba", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Introduction: Optimizer\n", @@ -248,7 +281,10 @@ "end_time": "2024-06-11T15:44:33.741130951Z", "start_time": "2024-06-11T15:44:33.734977769Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -259,7 +295,10 @@ "cell_type": "markdown", "id": "d26883eb74ce0d01", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Putting it all together\n", @@ -276,7 +315,10 @@ "end_time": "2024-06-11T15:44:41.730132530Z", "start_time": "2024-06-11T15:44:34.997777872Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -294,7 +336,10 @@ "end_time": "2024-06-11T15:44:41.738985151Z", "start_time": "2024-06-11T15:44:41.731989729Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -316,7 +361,10 @@ "cell_type": "markdown", "id": "6a8aab93b80fb82c", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "While here it is not going to be useful, we can also do multiple optimization steps in a loop! Do not forget to reset the gradients after each step!" @@ -330,7 +378,10 @@ "ExecuteTime": { "start_time": "2024-06-11T15:44:30.989940227Z" }, - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -342,7 +393,10 @@ "execution_count": null, "id": "a3a84aad4cd58737", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [] diff --git a/examples/notebooks/Prompt-Optimization.ipynb b/examples/notebooks/Tutorial-Prompt-Optimization.ipynb similarity index 100% rename from examples/notebooks/Prompt-Optimization.ipynb rename to examples/notebooks/Tutorial-Prompt-Optimization.ipynb diff --git a/requirements.txt b/requirements.txt index 82b52e6..e7d7d33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ platformdirs>=3.11.0 datasets>=2.14.6 diskcache>=5.6.3 graphviz>=0.20.3 -gdown>=5.2.0 \ No newline at end of file +gdown>=5.2.0 +pillow \ No newline at end of file diff --git a/setup.py b/setup.py index 9606119..0fad726 100644 --- a/setup.py +++ b/setup.py @@ -8,9 +8,9 @@ setup( name="textgrad", - version="0.1.3", + version="0.1.4", description="", - python_requires=">=3.8", + python_requires=">=3.9", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", diff --git a/textgrad/autograd/multimodal_ops.py b/textgrad/autograd/multimodal_ops.py index 6cd68a6..f6f2f35 100644 --- a/textgrad/autograd/multimodal_ops.py +++ b/textgrad/autograd/multimodal_ops.py @@ -19,6 +19,14 @@ class MultimodalLLMCall(Function): + """The MultiModalLM call function. This function will call the LLM with the input (image) and return the response, + also register the grad_fn for backpropagation. + + :param engine: engine to use for the LLM call + :type engine: EngineLM + :param system_prompt: system prompt to use for the LLM call, default depends on the engine. + :type system_prompt: Variable, optional + """ def __init__(self, engine: Union[str, EngineLM], system_prompt: Variable = None): @@ -34,6 +42,20 @@ def __init__(self, def forward(self, inputs: List[Variable], response_role_description: str = VARIABLE_OUTPUT_DEFAULT_ROLE) -> Variable: + """ + Forward pass for the multimodal LLM call function. + + :param inputs: list of input variables to the multimodal LLM call. One is an image and the second one is text + :type inputs: List[Variable] + :param response_role_description: role description for the response variable + :type response_role_description: str, optional + + >>> from textgrad import Variable, get_engine + >>> from textgrad.autograd import MultimodalLLMCall + >>> target_image = "A byte representation of the image" + >>> question_variable = Variable("What do you see here?", role_description="question to answer", requires_grad=False) + >>> response = MultimodalLLMCall("gpt-4o")([target_image, question_variable]) + """ # First ensure that all keys are present in the fields # Assert that all variables are either strings or bytes diff --git a/textgrad/engine/__init__.py b/textgrad/engine/__init__.py index 2eebcaf..b07aff5 100644 --- a/textgrad/engine/__init__.py +++ b/textgrad/engine/__init__.py @@ -49,4 +49,22 @@ def get_engine(engine_name: str, **kwargs) -> EngineLM: from .cohere import ChatCohere return ChatCohere(model_string=engine_name, **kwargs) else: - raise ValueError(f"Engine {engine_name} not supported") \ No newline at end of file + raise ValueError(f"Engine {engine_name} not supported") + + +def is_jpeg(data): + jpeg_signature = b'\xFF\xD8\xFF' + return data.startswith(jpeg_signature) + +def is_png(data): + png_signature = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' + return data.startswith(png_signature) + + +def get_image_type_from_bytes(data): + if is_jpeg(data): + return "jpeg" + elif is_png(data): + return "png" + else: + raise ValueError("Image type not supported, only jpeg and png supported.") \ No newline at end of file diff --git a/textgrad/engine/anthropic.py b/textgrad/engine/anthropic.py index b2a8f6d..2e90d8b 100644 --- a/textgrad/engine/anthropic.py +++ b/textgrad/engine/anthropic.py @@ -14,6 +14,7 @@ import json from typing import List, Union from .base import EngineLM, CachedEngine +from textgrad.engine import get_image_type_from_bytes class ChatAnthropic(EngineLM, CachedEngine): SYSTEM_PROMPT = "You are a helpful, creative, and smart assistant." @@ -83,7 +84,9 @@ def _format_content(self, content: List[Union[str, bytes]]) -> List[dict]: formatted_content = [] for item in content: if isinstance(item, bytes): - image_media_type = "image/jpeg" + image_type = get_image_type_from_bytes(item) + + image_media_type = f"image/{image_type}" base64_image = base64.b64encode(item).decode('utf-8') formatted_content.append( { "type": "image", diff --git a/textgrad/engine/openai.py b/textgrad/engine/openai.py index 0e755a9..f0ab2f3 100644 --- a/textgrad/engine/openai.py +++ b/textgrad/engine/openai.py @@ -13,6 +13,7 @@ wait_random_exponential, ) from typing import List, Union +from textgrad.engine import get_image_type_from_bytes from .base import EngineLM, CachedEngine @@ -92,11 +93,12 @@ def _format_content(self, content: List[Union[str, bytes]]) -> List[dict]: formatted_content = [] for item in content: if isinstance(item, bytes): + image_type = get_image_type_from_bytes(item) base64_image = base64.b64encode(item).decode('utf-8') formatted_content.append({ "type": "image_url", "image_url": { - "url": f"data:image/jpeg;base64,{base64_image}" + "url": f"data:image/{image_type};base64,{base64_image}" } }) elif isinstance(item, str):