你是否有过开发一个AI应用的想法,并且迫不及待地想要验证其是否可行呢?
你可能已经写好了AI部分的代码逻辑,但是还需要一个能够向他人展示你的创意,并让他们能够亲身体验的用户界面。这样,你才能更有效地验证你的想法,并收集用户的反馈加以改进,从而提升你的系统。
如果你正在寻找这样的解决方案,那么Gradio
就是你的理想选择。
Gradio是一个可以让你轻松、方便地使用Python代码构建一个友好Web交互界面的UI框架。它可以支持各种数据类型的输入和输出,还可以方便地与Hugging Face上的各种开源模型集成。
在这门课程中,我们将向你展示如何通过Gradio,仅用几行代码搭建一个交互程序,并利用Hugging Face上提供的一些现有模型,完成以下五个有趣的任务:
- 生成文本摘要:从一篇长文本中抽取出核心信息,生成一个简洁、精确的摘要。
- 命名实体识别:从文本中识别出人名、地名、组织名等实体,并给它们分类。
- 识别图像内容:从一张图片中识别出物体、场景、动作等内容,并用自然语言描述出来。
- 文本生成图像:根据一段描述性的文本,生成一张与之相符合的图片。
- 搭建基于LLM的聊天机器人:创建一个能够与用户进行自然对话的聊天机器人,并根据用户提供的信息生成个性化的回答。
如果你对这些任务感兴趣,那么请继续阅读本课程,一起探索Gradio和Hugging Face的魅力吧!
NLP任务接口
在前面的课程中,我们使用的都是ChatGPT一类的通用大型语言模型,但对于某些特定任务(如生成文本摘要)来说,使用一个专门针对该任务设计的小型专家模型(Small specialist model),也可以表现得与通用大型语言模型一样出色。
另一方面,小型专家模型可能还更便宜地运行,以及更快地响应用户。
在开始我们的任务之前,我们仍需完成以下两个前置步骤:
加载 API 密钥
python复制代码import os
# 首次运行,需执行以下指令:pip install python-dotenv
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv()) # 读取本地.env文件
hf_api_key = os.environ['HF_API_KEY']
HF_API_KEY
指的是 Hugging Face API 的访问密钥,可以通过以下步骤获取:
- 访问 Hugging Face 官网:huggingface.co/
- 鼠标移至右上角用户名-点击“Settings”。
- 点击“API tokens”选项。
- 点击“New token”按钮。
- 输入自定义的 API token 名称。
- 点击“Create new API token”,以生成一个新的 API token。
- 复制 API token 并保存到.env文件。
编写辅助函数
python复制代码import requests, json
def get_completion(inputs, parameters=None,ENDPOINT_URL=None):
headers = {
"Authorization": f"Bearer {hf_api_key}",
"Content-Type": "application/json"
}
data = { "inputs": inputs }
if parameters is not None:
data.update({"parameters": parameters})
response = requests.request("POST",
ENDPOINT_URL, headers=headers,
data=json.dumps(data)
)
return json.loads(response.content.decode("utf-8"))
该辅助函数是用于以API请求的方式访问指定端点进行推理,以完成特定任务的。
Hugging Face 提供了一个Inference API
,允许通过简单的 HTTP 请求,免费测试和评估超过 80,000 个可公开访问的机器学习模型或我们自己的私有模型,但有速率限制。
ini复制代码ENDPOINT_URL = https://api-inference.huggingface.co/models/<MODEL_ID>
<MODEL_ID>
表示我们要运行的模型,可以到 Hugging Face 的模型中心自由挑选适合我们应用业务场景的模型。
除了小型专家模型外,另外一个降低成本并提高速度的方法,就是基于大型模型来训练一个性能非常相似的较小模型,这个过程称为「蒸馏(Distillation)」。
比如我们接下来将使用的 shleifer/distilbart-cnn-12-6
,就是一个来自于 facebook/bart-large-cnn
的拥有 306M 参数的蒸馏模型。
bart-large-cnn
是文本摘要领域最先进的模型之一,由 Facebook 训练而成。
生成文本摘要
在这一部分里,我们将从一篇长文本中抽取出核心信息,生成一个简洁、精确的摘要。
步骤1:定义 summarise 的函数,接受输入,调用 getCompletion 函数,并返回摘要。
python复制代码API_URL = "https://api-inference.huggingface.co/models/sshleifer/distilbart-cnn-12-6"
def summarize(input):
output = get_completion(input, parameters = None, ENDPOINT_URL = API_URL)
return output[0]['summary_text']
步骤2:使用 Gradio 的 interface 函数,传入 summarise 函数 ,并将其输入和输出均设置为文本。
ini复制代码# 首次运行,需执行以下指令:pip install gradio
import gradio as gr
demo = gr.Interface(fn=summarize, inputs="text", outputs="text")
步骤3:调用 demo.launch 创建用户界面。
scss复制代码demo.launch()
运行结果如下:
(可选)步骤4:进一步优化用户界面,如添加标签、制定行数、增加标题和描述等。
ini复制代码demo = gr.Interface(fn=summarize,
inputs=[gr.Textbox(label="Text to summarize", lines=6)],
outputs=[gr.Textbox(label="Result", lines=3)],
title="Text summarization with distilbart-cnn",
description="Summarize any text using the `shleifer/distilbart-cnn-12-6` model under the hood!"
)
优化结果如下:
命名实体识别
在这一部分里,我们将从文本中识别出人名、地名、组织名等实体,并给它们分类。
这里使用到的是 dslim/bert-base-NER
模型,这是一个针对 NER (Named Entity Recognition,命名实体识别) 任务微调的包含 108M 参数 的 BERT
模型。
BERT 模型是一个用于自然语言处理的机器学习模型,该模型在解析一段文本时,可以识别出文本中包含的人名、组织名、地名等特定实体。
比如,当我们运行以下代码后,它就会输出一个包含多个字典的列表,每个字典都包含一个实体的信息。
ini复制代码API_URL = "https://api-inference.huggingface.co/models/dslim/bert-base-NER"
text = "My name is Andrew, I'm building DeepLearningAI and I live in California"
get_completion(text, parameters=None, ENDPOINT_URL= API_URL)
css复制代码[{ 'entity_group': 'PER', 'score': 0.9990624785423279, 'word': 'Andrew', 'start': 11, 'end': 17}, { 'entity_group': 'ORG', 'score': 0.896050214767456, 'word': 'DeepLearningAI', 'start': 32, 'end': 46}, { 'entity_group': 'LOC', 'score': 0.999692440032959, 'word': 'California', 'start': 61, 'end': 71}]
其中,entity_group键表示的含义分别是:
键值 | 含义 |
---|---|
PER | 人物 |
ORG | 组织机构 |
LOC | 位置 |
我们可以用 Gradio 让输出更加直观易懂:
步骤1:定义 ner 的函数,接受输入,调用 getCompletion 函数,并返回实体列表。
lua复制代码def ner(input):
output = get_completion(input, parameters=None, ENDPOINT_URL=API_URL)
for entity in output:
entity["entity"] = entity['entity_group']
return {"text": input, "entities": output}
步骤2:使用 Gradio 的 interface 函数,传入 ner 函数 ,将输入设置为文本,输出设置为高亮文本。
ini复制代码demo = gr.Interface(fn=ner,
inputs=[gr.Textbox(label="Text to find entities", lines=2)],
outputs=[gr.HighlightedText(label="Text with entities")],
title="NER with dslim/bert-base-NER",
description="Find entities using the `dslim/bert-base-NER` model under the hood!",
allow_flagging="never",
examples=["My name is Andrew and I live in California", "My name is Poli and work at HuggingFace"])
HighlightedText
组件的作用是接收并高亮显示NER模型输出的的实体。
examples
参数用于提供示例,帮助用户通快速了解程序是如何工作的。
步骤3:调用 demo.launch 创建用户界面。
scss复制代码demo.launch()
运行结果如下:
(可选)步骤4:将被拆分成多个标记的字符重组为一个完整的单词。
同样在前面的课程中我们有介绍过,LLM的处理单元不是一个个「单词」,而是一个个「标记(Token)」。它会接收一系列的字符,并将字符组合在一起,形成代表常见字符序列的标记。每个标记可能对应一个单词,或者空格,或者标点符号。
当我们选用另外一个示例时可以看到,HuggingFace这个单词会被分解为多个块,也即多个标记。
但是,由于我们可以从实体标签的开头字母判断单词的开头和中间部分:
字母 | 含义 |
---|---|
B | 开始标记 |
I | 中间标记 |
因此,我们可以定义一个 merge_tokens
函数,让每个标记在可视化时合并为一个完整单词显示,原理其实就是检查标签的开头字母,并进行合并:
less复制代码def merge_tokens(tokens):
merged_tokens = []
for token in tokens:
if merged_tokens and token['entity'].startswith('I-') and merged_tokens[-1]['entity'].endswith(token['entity'][2:]):
# 如果当前 token 延续了上一个 token 的实体,则将它们合并
last_token = merged_tokens[-1]
last_token['word'] += token['word'].replace('##', '')
last_token['end'] = token['end']
last_token['score'] = (last_token['score'] + token['score']) / 2
else:
# 否则,将 token 添加到列表中
merged_tokens.append(token)
return merged_tokens
再次运行,就可以看到正确的结果了:
python复制代码def ner(input):
output = get_completion(input, parameters=None, ENDPOINT_URL=API_URL)
merged_tokens = merge_tokens(output)
return {"text": input, "entities": merged_tokens}
图像描述应用
在这一部分里,我们将从一张图片中识别出物体、场景、动作等内容,并用自然语言描述出来。
这里使用到的是Salesforce/blip-image-captioning-base
模型,这是一个图像描述模型,可以将图像作为输入,并输出该图像的描述。
使用的免费图像来源于: free-images.com/
步骤1:定义 captioner 的函数,接受图像,调用 getCompletion 函数,并返回图像描述。
ini复制代码import io
import base64
# 将图像转为API所需的Base64格式
def image_to_base64_str(pil_image):
byte_arr = io.BytesIO()
pil_image.save(byte_arr, format='PNG')
byte_arr = byte_arr.getvalue()
return str(base64.b64encode(byte_arr).decode('utf-8'))
API_URL = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-base"
def captioner(image):
base64_image = image_to_base64_str(image)
result = get_completion(base64_image, parameters=None, ENDPOINT_URL=API_URL)
return result[0]['generated_text']
步骤2:使用 Gradio 的 interface 函数,传入 captioner 函数 ,将输入设置为图像,输出设置为文本。
ini复制代码demo = gr.Interface(fn=captioner,
inputs=[gr.Image(label="Upload image", type="pil")],
outputs=[gr.Textbox(label="Caption")],
title="Image Captioning with BLIP",
description="Caption any image using the BLIP model",
allow_flagging="never")
步骤3:调用 demo.launch 创建用户界面。
scss复制代码demo.launch()
运行结果如下:
图像生成应用
在这一部分里,我们将根据一段描述性的文本,生成一张与之相符合的图片。
这里使用到的是runwayml/stable-diffusion-v1-5
模型,也就是我们所熟知的Stable Diffusion
图像生成模型,我们使用API URL连接到了这个模型的服务器。
步骤1:定义 generate 的函数,接受文本描述,调用 getCompletion 函数,并返回图像。
ini复制代码# 将PIL图像转换为base64的辅助函数
# 这样你就可以将其发送到API
def base64_to_pil(img_base64):
base64_decoded = base64.b64decode(img_base64)
byte_stream = io.BytesIO(base64_decoded)
pil_image = Image.open(byte_stream)
return pil_image
API_URL = "https://api-inference.huggingface.co/models/Salesforce/blip-image-captioning-base"
def generate(prompt):
output = get_completion(prompt,parameters=None, ENDPOINT_URL=API_URL)
result_image = base64_to_pil(output)
return result_image
步骤2:使用 Gradio 的 interface 函数,传入 generate 函数 ,将输入设置为文本,输出设置为图像。
ini复制代码demo = gr.Interface(fn=generate,
inputs=[gr.Textbox(label="Your prompt")],
outputs=[gr.Image(label="Result")],
title="Image Generation with Stable Diffusion",
description="Generate any image with Stable Diffusion",
allow_flagging="never",
examples=["a dog in a park","a mecha robot in a favela"])
步骤3:调用 demo.launch 创建用户界面。
scss复制代码demo.launch()
运行结果如下:
(可选)步骤4:增加更多输入选项,以构建一个更高级的界面。
ini复制代码with gr.Blocks() as demo:
gr.Markdown("# Image Generation with Stable Diffusion")
with gr.Row(): # gr.Row()用于将组件水平排列
with gr.Column(scale=4): # scale值用于调整所占宽度比例
prompt = gr.Textbox(label="Your prompt")
with gr.Column(scale=1, min_width=50):
btn = gr.Button("Submit")
with gr.Accordion("Advanced options", open=False): # open=false表示默认折叠隐藏
negative_prompt = gr.Textbox(label="Negative prompt")
with gr.Row():
with gr.Column(): # gr.Column()用于将组件垂直排列
steps = gr.Slider(label="Inference Steps", minimum=1, maximum=100, value=25,
info="In many steps will the denoiser denoise the image?")
guidance = gr.Slider(label="Guidance Scale", minimum=1, maximum=20, value=7,
info="Controls how much the text prompt influences the result")
with gr.Column():
width = gr.Slider(label="Width", minimum=64, maximum=512, step=64, value=512)
height = gr.Slider(label="Height", minimum=64, maximum=512, step=64, value=512)
output = gr.Image(label="Result")
btn.click(fn=generate, inputs=[prompt,negative_prompt,steps,guidance,width,height], outputs=[output])
这些额外输入选项的作用如下:
标签 | 含义 | 类型 | 作用 |
---|---|---|---|
Your prompt | 提示 | 文本框 | 描述想要生成的图片内容 |
Negative prompt | 负提示 | 文本框 | 描述不想生成的图片内容 |
Inference Steps | 推理步骤 | 滑块控件 | 控制推理过程的执行步数,步数越多结果越精细 |
Guidance Scale | 指导尺度 | 滑块控件 | 控制文本提示对结果的影响程度,值越大生成结果越贴合提示内容 |
Width | 宽度 | 滑块控件 | 设置图片宽度,单位:像素 |
Height | 高度 | 滑块控件 | 设置图片高度,单位:像素 |
运行结果如下:
“描述与生成”游戏
在这一部分里,我们将结合前面两节的内容做一个游戏应用,首先识别一张图像的内容,再将识别出的内容作为描述生成另外一张图像。
步骤1:引入第3课和第4课的函数,captioner 函数用于接受图像并返回图像描述;generate 的函数用于接收图像描述并返回图像。
ini复制代码def image_to_base64_str(pil_image):
byte_arr = io.BytesIO()
pil_image.save(byte_arr, format='PNG')
byte_arr = byte_arr.getvalue()
return str(base64.b64encode(byte_arr).decode('utf-8'))
def base64_to_pil(img_base64):
base64_decoded = base64.b64decode(img_base64)
byte_stream = io.BytesIO(base64_decoded)
pil_image = Image.open(byte_stream)
return pil_image
def captioner(image):
base64_image = image_to_base64_str(image)
result = get_completion(base64_image, None, ITT_ENDPOINT)
return result[0]['generated_text']
def generate(prompt):
output = get_completion(prompt, None, TTI_ENDPOINT)
result_image = base64_to_pil(output)
return result_image
步骤2:使用 Gradio 的 interface 函数,传入合并了两个步骤的 caption_and_generate 函数 ,将输入设置为图像,输出分别设置为一个文本和一个图像。
ini复制代码def caption_and_generate(image):
caption = captioner(image)
image = generate(caption)
return [caption, image]
with gr.Blocks() as demo:
gr.Markdown("# Describe-and-Generate game 🖍️")
image_upload = gr.Image(label="Your first image",type="pil")
btn_all = gr.Button("Caption and generate")
caption = gr.Textbox(label="Generated caption")
image_output = gr.Image(label="Generated Image")
btn_all.click(fn=caption_and_generate, inputs=[image_upload], outputs=[caption, image_output])
gr.close_all()
步骤3:调用 demo.launch 创建用户界面。
scss复制代码demo.launch()
运行结果如下:
与任意LLM聊天
在这一部分里,我们将创建一个能够与用户进行自然对话的聊天机器人,并根据用户提供的信息生成个性化的回答。
这里使用的到是falcon-40b-instruct
模型,这是在Open LLM Leaderboard上排名最高的开源LLM之一。
步骤1:初始化Client,以借助text_generation库访问FalcomLM-instruct推理端点。
python复制代码import requests, json
from text_generation import Client
client = Client(os.environ['HF_API_FALCOM_BASE'], headers={"Authorization": f"Basic {hf_api_key}"}, timeout=120)
步骤2:定义 format_chat_prompt 函数,以格式化提示及对话历史
python复制代码def format_chat_prompt(message, chat_history, instruction):
prompt = f"System:{instruction}"
for turn in chat_history:
user_message, bot_message = turn
prompt = f"{prompt}nUser: {user_message}nAssistant: {bot_message}"
prompt = f"{prompt}nUser: {message}nAssistant:"
return prompt
其中涉及到三种角色消息我们也已经介绍过很多次了:
- 系统消息(System):负责指定LLM整体的语言风格或者助手的行为;
- 助手消息(Assistant):负责根据用户消息要求内容,以及系统消息的设定,输出一个合适的回应;
- 用户消息(User):给出一个具体的指令。
而这一步骤的目的是为模型提供记忆功能,使模型能够在理解上下文的前提下,回答后续问题。
vbnet复制代码User: what is the meaning of life?
Assistant:
User: what is the meaning of life?
Assistant: I'm sorry, I cannot provide a definitive answer to that question.It is a philosophical question
User: but why?
Assistant:
步骤3:定义 respond 函数,以调用 Client 的 generate 函数,将消息发送给模型,并将响应追加到对话历史。
ini复制代码def respond(message, chat_history):
formatted_prompt = format_chat_prompt(message, chat_history)
bot_message = client.generate(formatted_prompt,
max_new_tokens=1024,
stop_sequences=["nUser:", "<|endoftext|>"]).generated_text
chat_history.append((message, bot_message))
return "", chat_history
这里有一个问题。
随着这个过程的持续进行,我们发送给模型的对话历史会越来越多,直到模型达到一次对话中可以处理的最大标记数限制。
因此,我们选择在这里将最大标记数(max_new_tokens)参数设置为1024,这将保留最后1024个标记的对话历史。
要想详细这个参数的特点,可以参考之前《精华笔记:吴恩达 x LangChain《基于LangChain的大语言模型应用开发》(上)
》一文中的ConversationalTokenBufferMemory
。
而另外一个停止序列(stop_sequences)参数的作用,则是为了预防助手消息冒认用户消息,确保对话历史中的用户消息来自于真正的用户而不是来自于模型。
比如,当我们只向模型提问了这一句时:
模型可能会在回答的同时,构造多了一个虚假的提问:
步骤4:使用 Gradio 的 Chatbot 函数,快速构建一个聊天机器人组件
ini复制代码with gr.Blocks() as demo:
chatbot = gr.Chatbot(height=240)
msg = gr.Textbox(label="Prompt")
with gr.Accordion(label="Advanced options",open=False):
system = gr.Textbox(label="System message", lines=2, value="A conversation between a user and an LLM-based AI assistant. The assistant gives helpful and honest answers.")
temperature = gr.Slider(label="temperature", minimum=0.1, maximum=1, value=0.7, step=0.1)
btn = gr.Button("Submit")
clear = gr.ClearButton(components=[msg, chatbot], value="Clear console")
btn.click(respond, inputs=[msg, chatbot, system], outputs=[msg, chatbot])
msg.submit(respond, inputs=[msg, chatbot, system], outputs=[msg, chatbot]) #Press enter to submit
聊天机器人组件可以简化我们发送对话历史给模型的过程。
其中提供的一些额外输入选项的作用如下:
标签 | 含义 | 类型 | 作用 |
---|---|---|---|
System message | 系统消息 | 文本框 | 告诉LLM如何与你交流,比如设定角色或调整语气 |
temperature | 温度 | 滑块控件 | 为0时,模型对于相同输入会始终做出相同回答;值越大,模型给出的回答变化越大。 |
(可选)步骤5:让模型改用实时流式的方式传输答案
在这种方式下,我们会将标记逐个发送,并实时地看到它的完整回答,因此不需要等待整个答案准备好,最终呈现的效果就是与ChatGPT一样逐词输出。
ini复制代码def respond(message, chat_history, instruction, temperature=0.7):
prompt = format_chat_prompt(message, chat_history, instruction)
chat_history = chat_history + [[message, ""]]
stream = client.generate_stream(prompt,
max_new_tokens=1024,
stop_sequences=["nUser:", "<|endoftext|>"],
temperature=temperature)
#stop_sequences to not generate the user answer
acc_text = ""
#Streaming the tokens
for idx, response in enumerate(stream):
text_token = response.token.text
if response.details:
return
if idx == 0 and text_token.startswith(" "):
text_token = text_token[1:]
acc_text += text_token
last_turn = list(chat_history.pop(-1))
last_turn[-1] += acc_text
chat_history = chat_history + [last_turn]
yield "", chat_history
acc_text = ""
参考
Inference API-(huggingface.co/docs/api-in…)