TowardsDataScience-2023-博客中文翻译-十九-
TowardsDataScience 2023 博客中文翻译(十九)
使用地理空间关联规则挖掘发现便利店位置中的模式
理解东京便利店位置的空间趋势
·
关注 发表在Towards Data Science ·7 分钟阅读·2023 年 4 月 1 日
--
当你在东京四处逛逛时,你经常会经过很多便利店,本地人称之为“konbinis”,这是有道理的,因为在日本有超过 56,000家便利店。通常会有不同的便利店连锁店彼此靠近,很常见的是在街角或者马路对面看到两家店。考虑到东京的人口密度,竞争激烈的企业被迫靠近对方是可以理解的,但是这两家便利店连锁店之间可能存在某种关系吗?
定义任务
目标是从东京一个街区的许多便利店连锁店中收集位置数据,了解彼此之间是否存在任何关系。为了做到这一点,我们需要:
-
查询不同便利店的位置,检索每个店铺的名称和位置
-
找出在预定义半径内与彼此共同定位的便利店连锁店
-
使用共同位于的店铺数据来推导关联规则
-
在检查中绘制和可视化结果
让我们开始吧!
第一步:使用 OpenStreetMap 提取数据
对于我们的用例,我们想找到东京的便利店,所以首先我们需要做一些功课,了解常见的店铺连锁。一个快速的谷歌搜索告诉我,主要的店铺是FamilyMart、Lawson、7-Eleven、Ministop、Daily Yamazaki 和 NewDays。
现在我们知道我们要搜索什么了,让我们去OSMNX吧;这是一个在 OpenStreetMap(OSM)中搜索数据的很不错的 Python 包。根据 OSM 的架构,我们应该能在 'brand:en' 或 'brand' 字段中找到店铺名称。
我们可以首先导入一些有用的库来获取我们的数据,并定义一个函数,以返回指定区域内给定便利店连锁店的位置表:
import geopandas as gpd
from shapely.geometry import Point, Polygon
import osmnx
import shapely
import pandas as pd
import numpy as np
import networkx as nx
def point_finder(place, tags):
'''
Returns a dataframe of coordinates of an entity from OSM.
Parameters:
place (str): a location (i.e., 'Tokyo, Japan')
tags (dict): key value of entity attribute in OSM (i.e., 'Name') and value (i.e., amenity name)
Returns:
results (DataFrame): table of latitude and longitude with entity value
'''
gdf = osmnx.geocode_to_gdf(place)
#Getting the bounding box of the gdf
bounding = gdf.bounds
north, south, east, west = bounding.iloc[0,3], bounding.iloc[0,1], bounding.iloc[0,2], bounding.iloc[0,0]
location = gdf.geometry.unary_union
#Finding the points within the area polygon
point = osmnx.geometries_from_bbox(north,
south,
east,
west,
tags=tags)
point.set_crs(crs=4326)
point = point[point.geometry.within(location)]
#Making sure we are dealing with points
point['geometry'] = point['geometry'].apply(lambda x : x.centroid if type(x) == Polygon else x)
point = point[point.geom_type != 'MultiPolygon']
point = point[point.geom_type != 'Polygon']
results = pd.DataFrame({'name' : list(point['name']),
'longitude' : list(point['geometry'].x),
'latitude' : list(point['geometry'].y)}
)
results['name'] = list(tags.values())[0]
return results
convenience_stores = place_finder(place = 'Shinjuku, Tokyo',
tags={"brand:en" : " "})
我们可以传递每个便利店的名称并将结果合并为一个店铺名称、经度和纬度的表。对于我们的用例,我们可以关注东京的新宿区,看看每个便利店的丰富度如何:
便利店的频次统计。图片作者自拍。
显然,FamilyMart 和 7-Eleven 在店铺频次方面占主导地位,但空间上是什么样子的呢?当使用 Kepler.gl 绘制地理空间数据时,非常简单直观,它包括一个不错的界面,可用于创建可视化效果,可保存为 HTML 对象或直接在 Jupyter notebooks 中可视化:
新宿便利店的位置地图,店铺名称按颜色编码。图片作者自拍。
新宿便利店位置图,颜色编码的两分钟步行半径密度(168 米)。图片由作者提供。
第二步:寻找最近邻
现在我们有了数据,下一步是为每个便利店找到最近邻。为此,我们将使用 Scikit Learn 的 ‘BallTree’ 类 来寻找两分钟步行半径内的最近便利店名称。我们不关心多少家店被认为是最近邻,所以我们只关注在定义的半径内的便利店连锁。
# Convert location to radians
locations = convenience_stores[["latitude", "longitude"]].values
locations_radians = np.radians(locations)
# Create a balltree to search locations
tree = BallTree(locations_radians, leaf_size=15, metric='haversine')
# Find nearest neighbours in a 2 minute walking radius
is_within, distances = tree.query_radius(locations_radians, r=168/6371000, count_only=False, return_distance=True)
# Replace the neighbour indices with store names
df = pd.DataFrame(is_within)
df.columns = ['indices']
df['indices'] = [[val for val in row if val != idx] for idx, row in enumerate(df['indices'])]
# create temporary index column
convenience_stores = convenience_stores.reset_index()
# set temporary index column as index
convenience_stores = convenience_stores.set_index('index')
# create index-name mapping
index_name_mapping = convenience_stores['name'].to_dict()
# replace index values with names and remove duplicates
df['indices'] = df['indices'].apply(lambda lst: list(set(map(index_name_mapping.get, set(lst)))))
# Append back to original df
convenience_stores['neighbours'] = df['indices']
# Identify when a store has no neighbours
convenience_stores['neighbours'] = [lst if lst else ['no-neighbours'] for lst in convenience_stores['neighbours']]
# Unique store names
unique_elements = set([item for sublist in convenience_stores['neighbours'] for item in sublist])
# Count each stores frequency in the set of neighbours per location
counts = [dict(Counter(row)) for row in convenience_stores['neighbours']]
# Create a new dataframe with the counts
output_df = pd.DataFrame(counts).fillna(0)[sorted(unique_elements)]
如果我们想提高工作的准确性,我们可以用更准确的度量方式来替换哈弗辛距离(例如,通过使用 networkx 计算的步行时间),但我们会保持简单。
这将给我们一个数据框,其中每行对应一个位置,以及该位置附近哪些便利店连锁的二进制计数:
每个位置的便利店最近邻样本数据框。图片由作者提供。
第三步:关联规则挖掘
我们现在有一个数据集,准备进行关联规则挖掘。通过使用 mlxtend 库,我们可以使用 Apriori 算法 推导关联规则。最低支持度为 5%,这样我们只需检查数据集中与频繁出现相关的规则(例如,共同出现的便利店连锁)。我们在推导规则时使用‘lift’度量;lift 是同时包含前提和结论的地点比例相对于在独立假设下预期支持度的比率。
from mlxtend.frequent_patterns import association_rules, apriori
# Calculate apriori
frequent_set = apriori(output_df, min_support = 0.05, use_colnames = True)
# Create rules
rules = association_rules(frequent_set, metric = 'lift')
# Sort rules by the support value
rules.sort_values(['support'], ascending=False)
这将给我们以下结果表:
便利店数据的关联规则。图片由作者提供。
我们现在将解释这些关联规则,以获得一些高级的学习成果。要解释此表,最好阅读更多关于关联规则的内容,使用以下链接:
-
关联规则完整指南 —
towardsdatascience.com/association-rules-2-aa9a77241654
-
使用 Apriori 算法的关联规则 —
towardsdatascience.com/association-rules-with-apriori-algorithm-574593e35223
-
理解关联规则 —
towardsdatascience.com/a-simple-way-to-understand-association-rule-from-the-customer-basket-analysis-use-case-c7bcd75bdec1
好的,回到表格。
支持度告诉我们不同便利店连锁实际一起出现的频率。因此,我们可以说 7-Eleven 和 FamilyMart 在数据中约 31%的情况下同时出现。提升值超过 1 表明前提的存在增加了结论发生的可能性,这表明这两个连锁店的位置在某种程度上是相互依赖的。另一方面,7-Eleven 和 Lawson 之间的关联显示出更高的提升值,但置信度较低。
Daily Yamazaki 在我们的截止点附近支持度较低,并且与 FamilyMart 的位置显示出略高于 1 的弱关系。
其他规则是参考便利店的组合。例如,当 7-Eleven 和 FamilyMart 已经共同存在时,1.42 的高提升值表明它们与 Lawson 有较强的关联。
那么这为什么有用?
如果我们仅仅停留在找到每个商店位置的最近邻,我们就无法确定这些商店之间的关系。
地理空间关联规则为何对企业有洞察力的一个例子是确定新店位置。如果一个便利店连锁正在开设新位置,关联规则可以帮助识别哪些商店可能会共同出现。
当量身定制营销活动和定价策略时,这一点变得清晰,因为它提供了关于哪些商店可能会竞争的定量关系。由于我们知道 FamilyMart 和 7-Eleven 经常共同出现(我们通过关联规则证明了这一点),因此这两个连锁店都应更多关注它们的产品如何相对于其他连锁店(如 Lawson 和 Daily Yamazaki)进行竞争。
结论
在这篇文章中,我们为东京一个社区的便利店连锁创建了地理空间关联规则。这是通过从 OpenStreetMap 中提取数据、寻找最近邻便利店连锁、在地图上可视化数据以及使用 Apriori 算法创建关联规则来完成的。
感谢阅读!
使用 GPT-4 和 SpaCy 查找拼字游戏全字母句
原文:
towardsdatascience.com/finding-spelling-bee-pangrams-with-gpt-4-and-spacy-64d042969954
解决《纽约时报》拼图的探寻
·发表于 Towards Data Science ·7 分钟阅读·2023 年 3 月 21 日
--
Nemichandra Hombannavar拍摄的照片,来源于Unsplash
解决《纽约时报拼字游戏》可以是一次令人满意的经历,它在挑战与词汇探索的乐趣之间取得平衡。虽然这并不总是轻而易举,但找到每个单词所获得的满足感是值得付出努力的。在拼图中的各种语言成就中,发现全字母句就像发现了一块隐藏的宝藏。这个特别的词汇使用了所有给定的字母,突显了玩家在驾驭英语词汇丰富复杂性的技能。
发现全字母句对许多人来说是一项令人兴奋的活动,同时也为自然语言处理(NLP)练习提供了一个引人入胜的案例。SpaCy(Honnibal & Montani, 2017)是我最喜欢的工具,它是根据 MIT 许可开源的。你可以手动为 SpaCy 编写程序,但我想向你展示如何使用 GPT-4 开发这样的解决方案。
背景
拼字游戏
《纽约时报拼字游戏》是一个流行的单词拼图游戏,可以在《纽约时报》报纸和《纽约时报》网站上找到。在游戏中,玩家会得到一组七个字母,其中一个字母被指定为“中心”字母。游戏的目标是使用给定的字母尽可能多地创建单词,同时遵守以下规则:
-
每个单词必须至少四个字母长。
-
“中心”字母必须出现在每个单词中。
-
单词必须在英语词典中存在。
-
不允许使用专有名词和晦涩或冒犯性的词汇。
该游戏根据每个单词的长度分配点数。玩家获得一个四个字母单词的分数,每增加一个字母,分数会增加。全字母句是一个使用所有七个给定字母至少一次的单词,并且它会获得额外的分数。
参见人类如何解决难题:威廉·杰克逊·哈珀解答 NYT 拼字游戏 | 来源:YouTube.com
GPT-4
GPT,即生成预训练变换器,是由 OpenAI 开发的前沿 AI 语言模型,利用深度学习技术来理解和生成类似人类的文本。凭借其强大的变换器架构和对大量文本数据的预训练,GPT 能够在广泛的自然语言处理任务中表现出色,包括文本完成、翻译、摘要等。
SpaCy
SpaCy 是一个高性能的开源 Python 库,旨在处理高级自然语言处理(NLP)任务。由 Explosion AI 开发,SpaCy 提供了高效、生产就绪的文本处理、标记化、词性标注、命名实体识别、依存分析等工具。SpaCy 以速度和易用性为重点,使开发者能够快速构建自定义的 NLP 应用程序。
使用 GPT-4 开发 SpaCy 程序
准备工作
让我们准备工具链来开发 SpaCy。你应该已经安装了Python和pip,因为 SpaCy 是一个 Python 库。
要在 SpaCy 中编写一个程序来查找 NYT 拼字游戏中的全字母句,我们需要从词典或语料库中获取单词列表。在这个例子中,我将使用 NLTK(自然语言工具包)(Bird 等人,2009)库来获取单词列表。如果你还没有安装 NLTK,请先安装:
pip install nltk
然后,从 NLTK 下载‘words’语料库:
import nltk
nltk.download('words')
注意:NTLK 的单词语料库是一个使用于英语的单词列表。NLTK 中的单词语料库来源于“Words”数据集,该数据集最初来源于 Unix 的“Words”文件。该文件是一个包含在 Unix 系统中用于各种目的(例如拼写检查和生成随机文本)常用的英语单词列表。Unix 单词文件并非英语单词的详尽列表,可能包含一些古老或不常见的单词。它也缺乏专有名词,并可能不包括英语语言中较新的词汇。然而,它为各种自然语言处理任务,如单词分割、标记化和拼写校正,提供了一个有用的起点。
初次尝试
让我们向 GPT-4 提出一个简单的请求。
你能在 SpaCy 中编写一个程序来查找《纽约时报》拼字游戏中的全字母句吗?
GPT-4 提供的答案看起来很有希望。它包括设置 SpaCy 的步骤和以下 Python 程序。
import spacy
import nltk
from nltk.corpus import words
# Load the English language model
nlp = spacy.load("en_core_web_sm")
def is_pangram(word, required_letter, given_letters):
word = word.lower()
if required_letter not in word:
return False
return all(l in word for l in given_letters)
def find_pangrams(word_list, required_letter, given_letters):
pangrams = set()
for word in word_list:
if is_pangram(word, required_letter, given_letters):
pangrams.add(word)
return pangrams
def main():
# Get the list of words from nltk
nltk_words = words.words()
# Define the required letter and other given letters
required_letter = 'i'
given_letters = ['a', 'd', 'e', 'm', 't', 'y', 'i']
# Find pangrams
pangrams = find_pangrams(nltk_words, required_letter, given_letters)
print("Pangrams found:", pangrams)
if __name__ == "__main__":
main()
这是输出结果。
>>> pangrams
{'diathermotherapy', 'pentamethylenediamine', 'absentmindedly',
'undemonstratively', 'hypodermatically', 'bisdimethylamino',
'endolymphatic', 'Polystomidae', 'metasedimentary', 'Methodistically',
'thermodynamicist', 'vasoepididymostomy', 'animatedly', 'radiochemistry',
'hydrometeorological', 'demisability', 'hydrometallurgically',
'Mylodontidae', 'ektodynamorphic', 'detrimentally', 'Tetradynamia',
'immoderately', 'Merycopotamidae', 'Hypsiprymnodontinae', 'lymphadenitis',
'diaphanometry', 'Cyclostomatidae', 'Platystomidae', 'smithydander',
'hydroatmospheric', 'hydropneumatic', 'condimentary', 'hydrometamorphism', 'pentadactylism', 'diathermacy', 'ventripyramid', 'acidometry', 'thermodynamist', 'improvidentially', 'methodically', 'diethylenediamine', 'aldermanity', 'hemidactylous', 'myxadenitis', 'Myliobatidae', 'demountability', 'diathermy', 'lymphangioendothelioma', 'unmitigatedly', 'Basidiomycetes', 'Hydrometridae', 'ectodactylism', 'thermoradiotherapy', 'predaytime', 'indemnificatory', 'Hemibasidiomycetes', 'dilemmatically', 'cardiometry', 'lymphadenectasia', 'misunderstandingly', 'diastematomyelia',
经过仔细观察,我们发现它包括一些不在给定字母列表中的字母。
第二次尝试
通过我们的观察,我们可以要求 GPT 进行改进。
程序无法正常工作。它返回了包含“given_letters”的单词,但我们需要的是仅包含 given_letters 的单词。它不能有其他字母。
GPT-4 重写了程序。
import nltk
from nltk.corpus import words
def is_pangram(word, required_letter, given_letters):
word = word.lower()
if required_letter not in word:
return False
for letter in word:
if letter not in given_letters:
return False
return all(l in word for l in given_letters)
def find_pangrams(word_list, required_letter, given_letters):
pangrams = set()
for word in word_list:
if is_pangram(word, required_letter, given_letters):
pangrams.add(word)
return pangrams
def main():
# Get the list of words from nltk
nltk_words = words.words()
# Define the required letter and other given letters
required_letter = 'i'
given_letters = ['a', 'd', 'e', 'm', 't', 'y', 'i']
# Find pangrams
pangrams = find_pangrams(nltk_words, required_letter, given_letters)
print("Pangrams found:", pangrams)
if __name__ == "__main__":
main()
这次的输出是三个单词。NYTimes 拼字比赛中要找的单词是“daytime”。
>>> pangrams
{'didymate', 'daytime', 'Maytide'}
我们可以称之为胜利吗?
NYTimes 拼字比赛的截图:1) 2023 年 5 月 16 日 2) 2023 年 5 月 17 日 3) 2023 年 5 月 18 日 | 截图由 Sean Zhai 提供
异常与解决方案
2023 年 5 月 17 日的拼字比赛有以下给定字母:C D E N L O W,但我们的程序返回了一个空输出。它无法找到一个全字句。
这时我们需要帮助 GPT-4。我尝试让 GPT-4 分析错误,但效果不好,于是我介入了。算法看起来是正确的,我接下来的检查是 NLTK 语料库。我在 Python 中对当天的全字句——clowned 进行了快速搜索,但没有找到。所以我们找到了问题。
我陈述了问题,并要求 GPT-4 提供新解决方案。
在用于查找全字句的 Python 程序中,nltk.corpus 中的“words”没有涵盖 NYTimes 使用的所有单词。例如,“clowned” 就没有包括在内。有没有更好的地方可以获取“words”?
GPT-4 认识到问题,并提供了新的英语单词来源及更新后的 Python 程序。
一个更全面的英语单词来源是 words.txt
文件,它来源于 SCOWL(拼写检查器导向单词列表)项目。你可以在 GitHub 仓库中找到它: github.com/dwyl/english-words
def read_words_from_file(filename):
with open(filename, 'r') as f:
return [word.strip() for word in f.readlines()]
def is_pangram(word, required_letter, given_letters):
word = word.lower()
if len(word) < 4 or required_letter not in word:
return False
for letter in word:
if letter not in given_letters:
return False
return all(l in word for l in given_letters)
def find_pangrams(word_list, required_letter, given_letters):
pangrams = set()
for word in word_list:
if is_pangram(word, required_letter, given_letters):
pangrams.add(word)
return pangrams
def main():
# Get the list of words from the words.txt file
filename = 'words.txt'
word_list = read_words_from_file(filename)
# Define the required letter and other given letters
required_letter = 'c'
given_letters = ['c', 'd', 'e', 'n', 'l', 'o', 'w']
# Find pangrams
pangrams = find_pangrams(word_list, required_letter, given_letters)
print("Pangrams found:", pangrams)
if __name__ == "__main__":
main()
将新程序保存为 pangram.py。现在结果如下:
% python3 pangram.py
Pangrams found: {'clowned'}
满意!
讨论
将 GPT-4 应用于实际任务是测试程序能力的有趣方式。它确实令人印象深刻,并将改变我们的工作方式。以下是一些可能在给 GPT-4 提示时有所帮助的事项。
-
具体明确。为你的问题定义一个合适的范围。
-
给出例子以说明你的陈述。
-
与 GPT-4 合作,并始终验证输出结果。
-
运用自己的思维分析复杂情况。
参考文献
Honnibal, M., & Montani, I. (2017). spaCy 2: 使用 Bloom 嵌入、卷积神经网络和增量解析进行自然语言理解。
Bird, S., Klein, E., & Loper, E. (2009). 使用 Python 进行自然语言处理:用自然语言工具包分析文本。O’Reilly Media, Inc.
在 Twitter 帖子中寻找时间模式:使用 Python 进行探索性数据分析
使用 Python、K-Means 和 t-SNE 对 Twitter 数据进行聚类
·发表于 Towards Data Science ·17 分钟阅读·2023 年 5 月 26 日
--
Tweet clusters t-SNE 可视化,图像由作者提供
在文章 “人们对气候的看法” 中,我使用自然语言处理、向量化和聚类分析了 Twitter 帖子。通过这种技术,可以在非结构化文本数据中找到不同的群体,例如,从数千条关于气候的推文中提取有关冰川融化或电动交通的消息。在处理这些数据时,另一个问题出现了:如果我们将相同的算法应用于这些消息发布的时间而不是消息本身会怎样?这将使我们能够分析何时和多频繁不同的人在社交媒体上发帖。这不仅从社会学或心理学的角度重要,正如我们稍后将看到的,还可以用来检测机器人或发送垃圾信息的用户。最后但同样重要的是,几乎每个人现在都在使用社交平台,因此了解有关我们的新事物是很有趣的。显然,相同的算法不仅可以用于 Twitter 帖子,还可以用于任何媒体平台。
方法论
我将使用与第一部分 Twitter 数据分析中描述的相似的方法。我们的数据处理流程将包括几个步骤:
-
收集包含特定标签的推文并将其保存到 CSV 文件中。这在之前的文章中已经完成,所以我在这里跳过详细说明。
-
找到收集的数据的一般属性。
-
根据用户发帖的时间计算每个用户的嵌入向量。
-
使用 K-Means 算法对数据进行聚类。
-
分析结果。
让我们开始吧。
1. 加载数据
我将使用Tweepy库来收集 Twitter 帖子。更多细节可以在第一部分中找到;这里我只发布源代码:
import tweepy
api_key = "YjKdgxk..."
api_key_secret = "Qa6ZnPs0vdp4X...."
auth = tweepy.OAuth2AppHandler(api_key, api_key_secret)
api = tweepy.API(auth, wait_on_rate_limit=True)
hashtag = "#climate"
language = "en"
def text_filter(s_data: str) -> str:
""" Remove extra characters from text """
return s_data.replace("&", "and").replace(";", " ").replace(",", " ") \
.replace('"', " ").replace("\n", " ").replace(" ", " ")
def get_hashtags(tweet) -> str:
""" Parse retweeted data """
hash_tags = ""
if 'hashtags' in tweet.entities:
hash_tags = ','.join(map(lambda x: x["text"], tweet.entities['hashtags']))
return hash_tags
def get_csv_header() -> str:
""" CSV header """
return "id;created_at;user_name;user_location;user_followers_count;user_friends_count;retweets_count;favorites_count;retweet_orig_id;retweet_orig_user;hash_tags;full_text"
def tweet_to_csv(tweet):
""" Convert a tweet data to the CSV string """
if not hasattr(tweet, 'retweeted_status'):
full_text = text_filter(tweet.full_text)
hasgtags = get_hashtags(tweet)
retweet_orig_id = ""
retweet_orig_user = ""
favs, retweets = tweet.favorite_count, tweet.retweet_count
else:
retweet = tweet.retweeted_status
retweet_orig_id = retweet.id
retweet_orig_user = retweet.user.screen_name
full_text = text_filter(retweet.full_text)
hasgtags = get_hashtags(retweet)
favs, retweets = retweet.favorite_count, retweet.retweet_count
s_out = f"{tweet.id};{tweet.created_at};{tweet.user.screen_name};{addr_filter(tweet.user.location)};{tweet.user.followers_count};{tweet.user.friends_count};{retweets};{favs};{retweet_orig_id};{retweet_orig_user};{hasgtags};{full_text}"
return s_out
if __name__ == "__main__":
pages = tweepy.Cursor(api.search_tweets, q=hashtag, tweet_mode='extended',
result_type="recent",
count=100,
lang=language).pages(limit)
with open("tweets.csv", "a", encoding="utf-8") as f_log:
f_log.write(get_csv_header() + "\n")
for ind, page in enumerate(pages):
for tweet in page:
# Get data per tweet
str_line = tweet_to_csv(tweet)
# Save to CSV
f_log.write(str_line + "\n")
使用这段代码,我们可以获取在过去 7 天内发布了特定标签的所有 Twitter 帖子。标签实际上是我们的搜索查询,我们可以找到有关气候、政治或任何其他主题的帖子。可选地,语言代码允许我们搜索不同语言的帖子。读者可以自行进行额外的研究;例如,比较英语和西班牙语推文之间的结果可能会很有趣。
在保存 CSV 文件后,让我们将其加载到数据框中,删除不需要的列,并查看我们拥有的数据:
import pandas as pd
df = pd.read_csv("climate.csv", sep=';', dtype={'id': object, 'retweet_orig_id': object, 'full_text': str, 'hash_tags': str}, parse_dates=["created_at"], lineterminator='\n')
df.drop(["retweet_orig_id", "user_friends_count", "retweets_count", "favorites_count", "user_location", "hash_tags", "retweet_orig_user", "user_followers_count"], inplace=True, axis=1)
df = df.drop_duplicates('id')
with pd.option_context('display.max_colwidth', 80):
display(df)
与第一部分相同,我获取了带有“#climate”标签的 Twitter 帖子。结果如下:
我们实际上不需要文本或用户 ID,但它对于“调试”是有用的,可以查看原始推文的样子。为了未来的处理,我们需要知道每条推文的日期、时间和小时。让我们向数据框添加列:
def get_time(dt: datetime.datetime):
""" Get time and minute from datetime """
return dt.time()
def get_date(dt: datetime.datetime):
""" Get date from datetime """
return dt.date()
def get_hour(dt: datetime.datetime):
""" Get time and minute from datetime """
return dt.hour
df["date"] = df['created_at'].map(get_date)
df["time"] = df['created_at'].map(get_time)
df["hour"] = df['created_at'].map(get_hour)
我们可以轻松验证结果:
display(df[["user_name", "date", "time", "hour"]])
现在我们拥有了所有需要的信息,可以开始进行分析。
2. 一般见解
从最后的截图中我们可以看到,已加载了 199,278 条消息;这些是带有“#Climate”标签的消息,我在几周内收集的。作为热身,让我们回答一个简单的问题:人们平均每天发布多少关于气候的消息?
首先,让我们计算总天数和用户总数:
days_total = df['date'].unique().shape[0]
print(days_total)
# > 46
users_total = df['user_name'].unique().shape[0]
print(users_total)
# > 79985
如我们所见,数据收集了 46 天,总共有 79,985 名 Twitter 用户在这段时间内发布了(或转发了)至少一条带有“#Climate”标签的消息。显然,我们只能计算至少发布过一条消息的用户;遗憾的是,我们无法通过这种方式获得读者的数量。
让我们找出每个用户的每日消息数量。首先,我们按用户名对数据框进行分组:
gr_messages_per_user = df.groupby(['user_name'], as_index=False).size().sort_values(by=['size'], ascending=False)
gr_messages_per_user["size_per_day"] = gr_messages_per_user['size'].div(days_total)
“size”列显示了每个用户发送的消息数量。我还添加了“size_per_day”列,通过将消息总数除以总天数即可轻松计算。结果如下:
我们可以看到,最活跃的用户每天最多发布 18 条消息,而最不活跃的用户在这 46 天的时间里只发布了 1 条消息(1/46 = 0.0217)。让我们使用 NumPy 和Bokeh绘制直方图:
import numpy as np
from bokeh.io import show, output_notebook, export_png
from bokeh.plotting import figure, output_file
from bokeh.models import ColumnDataSource, LabelSet, Whisker
from bokeh.transform import factor_cmap, factor_mark, cumsum
from bokeh.palettes import *
output_notebook()
users = gr_messages_per_user['user_name']
amount = gr_messages_per_user['size_per_day']
hist_e, edges_e = np.histogram(amount, density=False, bins=100)
# Draw
p = figure(width=1600, height=500, title="Messages per day distribution")
p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], line_color="darkblue")
p.x_range.start = 0
# p.x_range.end = 150000
p.y_range.start = 0
p.xaxis[0].ticker.desired_num_ticks = 20
p.left[0].formatter.use_scientific = False
p.below[0].formatter.use_scientific = False
p.xaxis.axis_label = "Messages per day, avg"
p.yaxis.axis_label = "Amount of users"
show(p)
输出如下:
每日消息数量分布,图像由作者提供
有趣的是,我们只能看到一根柱子。在所有 79,985 名使用“#Climate”标签发布消息的用户中,几乎所有人(77,275 名用户)的平均每天发送的消息不到一条。乍一看,这似乎很令人惊讶,但实际上,我们有多频繁地发布关于气候的推文呢?说实话,我一生中从未做过。我们需要将图表放大很多才能看到直方图上的其他柱子:
每天消息的分布情况(更高的缩放),图像由作者提供
只有在这个缩放级别下,我们才能看到在所有 79,985 名关于“#Climate”发过消息的 Twitter 用户中,只有不到 100 名“活跃分子”每天发消息!好吧,也许“气候”不是人们每天发帖的内容,但其他话题是否也如此?我创建了一个辅助函数,返回每天发布超过 N 条消息的“活跃”用户的百分比。
def get_active_users_percent(df_in: pd.DataFrame, messages_per_day_threshold: int):
""" Get percentage of active users with a messages-per-day threshold """
days_total = df_in['date'].unique().shape[0]
users_total = df_in['user_name'].unique().shape[0]
gr_messages_per_user = df_in.groupby(['user_name'], as_index=False).size()
gr_messages_per_user["size_per_day"] = gr_messages_per_user['size'].div(days_total)
users_active = gr_messages_per_user[gr_messages_per_user['size_per_day'] >= messages_per_day_threshold].shape[0]
return 100*users_active/users_total
然后,使用相同的 Tweepy 代码,我从不同领域下载了 6 个话题的数据框。我们可以用 Bokeh 绘制结果:
labels = ['#Climate', '#Politics', '#Cats', '#Humour', '#Space', '#War']
counts = [get_active_users_percent(df_climate, messages_per_day_threshold=1),
get_active_users_percent(df_politics, messages_per_day_threshold=1),
get_active_users_percent(df_cats, messages_per_day_threshold=1),
get_active_users_percent(df_humour, messages_per_day_threshold=1),
get_active_users_percent(df_space, messages_per_day_threshold=1),
get_active_users_percent(df_war, messages_per_day_threshold=1)]
palette = Spectral6
source = ColumnDataSource(data=dict(labels=labels, counts=counts, color=palette))
p = figure(width=1200, height=400, x_range=labels, y_range=(0,9),
title="Percentage of Twitter users posting 1 or more messages per day",
toolbar_location=None, tools="")
p.vbar(x='labels', top='counts', width=0.9, color='color', source=source)
p.xgrid.grid_line_color = None
p.y_range.start = 0
show(p)
结果很有趣:
每天发布至少 1 条消息的活跃用户百分比,使用特定标签
这里最受欢迎的标签是“#Cats”。在这个组中,大约 6.6%的用户每天发帖。他们的猫只是太可爱了吗,无法抗拒诱惑?相反,“#Humour”是一个热门话题,消息量很大,但每天发多条消息的人数最少。在“#War”或“#Politics”这样的更严肃的话题中,大约 1.5%的用户每天发帖。令人惊讶的是,相较于“#Humour”,更多人每天发布关于“#Space”的消息。
为了更详细地澄清这些数字,让我们找出每个用户消息数量的分布;虽然这与消息时间没有直接关系,但仍然有趣的是找到答案:
def get_cumulative_percents_distribution(df_in: pd.DataFrame, steps=200):
""" Get a distribution of total percent of messages sent by percent of users """
# Group dataframe by user name and sort by amount of messages
df_messages_per_user = df_in.groupby(['user_name'], as_index=False).size().sort_values(by=['size'], ascending=False)
users_total = df_messages_per_user.shape[0]
messages_total = df_messages_per_user["size"].sum()
# Get cumulative messages/users ratio
messages = []
percentage = np.arange(0, 100, 0.05)
for perc in percentage:
msg_count = df_messages_per_user[:int(perc*users_total/100)]["size"].sum()
messages.append(100*msg_count/messages_total)
return percentage, messages
这种方法计算了最活跃用户发布的消息总数。具体的数量对于不同的话题可能会有很大的差异,因此我使用百分比作为输出。通过这个功能,我们可以比较不同标签的结果:
# Calculate
percentage, messages1 = get_cumulative_percent(df_climate)
_, messages2 = get_cumulative_percent(df_politics)
_, messages3 = get_cumulative_percent(df_cats)
_, messages4 = get_cumulative_percent(df_humour)
_, messages5 = get_cumulative_percent(df_space)
_, messages6 = get_cumulative_percent(df_war)
labels = ['#Climate', '#Politics', '#Cats', '#Humour', '#Space', '#War']
messages = [messages1, messages2, messages3, messages4, messages5, messages6]
# Draw
palette = Spectral6
p = figure(width=1200, height=400,
title="Twitter messages per user percentage ratio",
x_axis_label='Percentage of users',
y_axis_label='Percentage of messages')
for ind in range(6):
p.line(percentage, messages[ind], line_width=2, color=palette[ind], legend_label=labels[ind])
p.x_range.end = 100
p.y_range.start = 0
p.y_range.end = 100
p.xaxis.ticker.desired_num_ticks = 10
p.legend.location = 'bottom_right'
p.toolbar_location = None
show(p)
因为两个轴都被“标准化”为 0..100%,所以很容易比较不同话题的结果:
最活跃用户的消息分布,图像由作者提供
再次,结果看起来很有趣。我们可以看到分布有很强的偏斜:10%最活跃的用户发布了 50-60%的消息(剧透:正如我们很快会看到的,并不是所有人都是人类;)。
这个图表是通过一个只有大约 20 行代码的函数制作的。这个“分析”相当简单,但可能会出现许多额外的问题。不同话题之间有明显的区别,找出原因显然不是简单的。哪些话题有最多的活跃用户?是否存在文化或地域差异,不同国家如美国、俄罗斯或日本的曲线是否相同?我鼓励读者自己做一些测试。
既然我们获得了一些基本见解,是时候做一些更具挑战性的事情了。让我们对所有用户进行聚类,并尝试寻找一些共同模式。为此,首先,我们需要将用户的数据转换为嵌入向量。
3. 创建用户嵌入
嵌入向量是表示每个用户数据的一系列数字。在上一篇文章中,我从推文的词汇和句子中获取了嵌入向量。现在,因为我想在“时间”领域找到模式,我将基于消息时间计算嵌入。但首先,让我们了解一下数据的样子。
作为提醒,我们有一个包含所有推文的数据框,这些推文是为了特定的标签收集的。每条推文都有一个用户名、创建日期、时间和小时:
让我们创建一个帮助函数来显示特定用户的所有推文时间:
def draw_user_timeline(df_in: pd.DataFrame, user_name: str):
""" Draw cumulative messages time for specific user """
df_u = df_in[df_in["user_name"] == user_name]
days_total = df_u['date'].unique().shape[0]
# Group messages by time of the day
messages_per_day = df_u.groupby(['time'], as_index=False).size()
msg_time = messages_per_day['time']
msg_count = messages_per_day['size']
# Draw
p = figure(x_axis_type='datetime', width=1600, height=150, title=f"Cumulative tweets timeline during {days_total} days: {user_name}")
p.vbar(x=msg_time, top=msg_count, width=datetime.timedelta(seconds=30), line_color='black')
p.xaxis[0].ticker.desired_num_ticks = 30
p.xgrid.grid_line_color = None
p.toolbar_location = None
p.x_range.start = datetime.time(0,0,0)
p.x_range.end = datetime.time(23,59,0)
p.y_range.start = 0
p.y_range.end = 1
show(p)
draw_user_timeline(df, user_name="UserNameHere")
...
结果看起来是这样的:
几个用户的消息时间线,作者提供的图片
在这里,我们可以看到一些用户在几周内发送的消息,显示在 00-24 小时的时间轴上。我们可能已经看到了一些模式,但事实证明,有一个问题。Twitter API 不返回时区。消息正文中有一个“timezone”字段,但它总是为空。也许当我们在浏览器中查看推文时,我们看到的是本地时间;在这种情况下,原始时区就不那么重要了。或者这可能是免费账户的一个限制。无论如何,如果来自美国的一个用户在 UTC 2 AM 开始发送消息,而来自印度的另一个用户在 UTC 13 PM 开始发送消息,两者的时间线将无法匹配在一起。
作为解决办法,我尝试通过使用一个简单的经验规则来“估计”时区:大多数人在夜间睡觉,很可能他们在那个时候不会发推文 😉 因此,我们可以找到一个 9 小时的时间段,在这个时间段内消息的平均数量最少,并假设这是该用户的“夜间”时间。
def get_night_offset(hours: List):
""" Estimate the night position by calculating the rolling average minimum """
night_len = 9
min_pos, min_avg = 0, 99999
# Find the minimum position
data = np.array(hours + hours)
for p in range(24):
avg = np.average(data[p:p + night_len])
if avg <= min_avg:
min_avg = avg
min_pos = p
# Move the position right if possible (in case of long sequence of similar numbers)
for p in range(min_pos, len(data) - night_len):
avg = np.average(data[p:p + night_len])
if avg <= min_avg:
min_avg = avg
min_pos = p
else:
break
return min_pos % 24
def normalize(hours: List):
""" Move the hours array to the right, keeping the 'night' time at the left """
offset = get_night_offset(hours)
data = hours + hours
return data[offset:offset+24]
实际上,这在可以轻松检测到“夜间”时间段的情况下效果良好:
“活跃”用户的消息时间线,作者提供的图片
当然,有些人早上 7 点醒来,有些人则是早上 10 点,且没有时区信息,我们无法找到它。总之,这总比没有好,作为一个“基线”,这个算法可以使用。
显然,算法在这种情况下不起作用:
另一个只有几个“活跃”小时的用户,图像来源:作者
在这个例子中,我们只是不知道这个用户是在早晨、晚上还是午餐后发布消息;对此没有信息。但仍然有趣的是看到一些用户仅在一天中的特定时间发布消息。在这种情况下,拥有一个“虚拟偏移”仍然有帮助;它使我们能够“对齐”所有用户时间线,正如我们在结果中即将看到的那样。
现在让我们计算嵌入向量。可以有不同的方法来做到这一点。我决定使用* [SumTotal, Sum00,.., Sum23]* 形式的向量,其中 SumTotal 是用户发布的总消息量,而 Sum00..Sum23 是每天每小时发布的总消息数。我们可以使用 Pandas 的 groupby 方法,参数为“user_name”和“hour”,这几乎可以为我们完成所有需要的计算:
def get_vectorized_users(df_in: pd.DataFrame):
""" Get embedding vectors for all users
Embedding format: [total hours, total messages per hour-00, 01, .. 23]
"""
gr_messages_per_user = df_in.groupby(['user_name', 'hour'], as_index=True).size()
vectors = []
users = gr_messages_per_user.index.get_level_values('user_name').unique().values
for ind, user in enumerate(users):
if ind % 10000 == 0:
print(f"Processing {ind} of {users.shape[0]}")
hours_all = [0]*24
for hr, value in gr_messages_per_user[user].items():
hours_all[hr] = value
hours_norm = normalize(hours_all)
vectors.append([sum(hours_norm)] + hours_norm)
return users, np.asarray(vectors)
all_users, vectorized_users = get_vectorized_users(df)
在这里,“get_vectorized_users”方法正在进行计算。计算每个 00..24 小时向量后,我使用“normalize”函数来应用之前描述的“时区”偏移。
实际上,对于一个相对活跃的用户,嵌入向量可能如下所示:
[120 0 0 0 0 0 0 0 0 0 1 2 0 2 2 1 0 0 0 0 0 18 44 50 0]
这里 120 是消息的总数,其余部分是一个 24 位的数组,其中包含每小时发布的消息数量(作为提醒,在我们的案例中,数据是在 46 天内收集的)。对于不活跃的用户,嵌入可能如下所示:
[4 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0]
也可以创建不同的嵌入向量,更复杂的方案可能会提供更好的结果。例如,添加每天“活跃”小时的总数或将星期几包含到向量中,看看用户活动如何在工作日和周末之间变化,等等,可能会很有趣。
4. 聚类
和上一篇文章一样,我将使用 K-Means 算法来寻找聚类。首先,让我们使用肘部法则来找到最佳 K 值:
import matplotlib.pyplot as plt
%matplotlib inline
def graw_elbow_graph(x: np.array, k1: int, k2: int, k3: int):
k_values, inertia_values = [], []
for k in range(k1, k2, k3):
print("Processing:", k)
km = KMeans(n_clusters=k).fit(x)
k_values.append(k)
inertia_values.append(km.inertia_)
plt.figure(figsize=(12,4))
plt.plot(k_values, inertia_values, 'o')
plt.title('Inertia for each K')
plt.xlabel('K')
plt.ylabel('Inertia')
graw_elbow_graph(vectorized_users, 2, 20, 1)
结果如下所示:
用户嵌入的肘部图,图像来源:作者
让我们编写方法来计算聚类并绘制一些用户的时间线:
def get_clusters_kmeans(x, k):
""" Get clusters using K-Means """
km = KMeans(n_clusters=k).fit(x)
s_score = silhouette_score(x, km.labels_)
print(f"K={k}: Silhouette coefficient {s_score:0.2f}, inertia:{km.inertia_}")
sample_silhouette_values = silhouette_samples(x, km.labels_)
silhouette_values = []
for i in range(k):
cluster_values = sample_silhouette_values[km.labels_ == i]
silhouette_values.append((i, cluster_values.shape[0], cluster_values.mean(), cluster_values.min(), cluster_values.max()))
silhouette_values = sorted(silhouette_values, key=lambda tup: tup[2], reverse=True)
for s in silhouette_values:
print(f"Cluster {s[0]}: Size:{s[1]}, avg:{s[2]:.2f}, min:{s[3]:.2f}, max: {s[4]:.2f}")
print()
# Create new dataframe
data_len = x.shape[0]
cdf = pd.DataFrame({
"id": all_users,
"vector": [str(v) for v in vectorized_users],
"cluster": km.labels_,
})
# Show top clusters
for cl in silhouette_values[:10]:
df_c = cdf[cdf['cluster'] == cl[0]]
# Show cluster
print("Cluster:", cl[0], cl[2])
with pd.option_context('display.max_colwidth', None):
display(df_c[["id", "vector"]][:20])
# Show first users
for user in df_c["id"].values[:10]:
draw_user_timeline(df, user_name=user)
print()
return km.labels_
clusters = get_clusters_kmeans(vectorized_users, k=5)
这个方法与前面部分大致相同,唯一的区别是我为每个聚类绘制用户时间线,而不是词云。
5. 结果
最终,我们准备查看结果。显然,并不是所有组都完美分开,但一些类别值得提及。作为提醒,我分析了所有在 46 天内发布了“#Climate”标签的用户的推文。那么,在气候相关的帖子中,我们可以看到哪些聚类呢?
“非活跃” 用户,他们在一个月内只发送了 1–2 条消息。这个群体是最大的;正如前面讨论的,它代表了所有用户的 95%以上。而 K-Means 算法能够检测到这个群体是最大的。那些用户的时间线如下:
几个“非活跃”用户的消息时间线,图像来源于作者
“感兴趣” 用户。这些用户每 2–5 天发布一次推文,因此我可以假设他们对这个话题至少有一定的兴趣。
几个“感兴趣”用户的消息时间线,图像来源于作者
“活跃” 用户。这些用户每天发布超过几条消息:
几个“活跃”用户的消息时间线,图像来源于作者
我们不知道这些人是只是“积极分子”,还是将发布推文作为工作的一个部分,但至少我们可以看到他们的在线活动相当高。
“机器人”。这些用户极不可能是人类。毫不奇怪,他们发布的消息数量最多。当然,我没有 100%证据证明所有这些账户都属于机器人,但任何人类都不可能如此规律地发布消息而没有休息和睡眠:
几个“机器人”的消息时间线,图像来源于作者
例如,第二个“用户”在一天中的同一时间发布推文,精确到 1 秒;他的推文可以用作 NTP 服务器 😃
顺便说一下,一些其他的“用户”实际上并不活跃,但他们的时间线看起来很可疑。这个“用户”消息不多,而且存在明显的“日/夜”模式,所以它没有被聚类为“机器人”。但对我来说,普通用户严格在每小时开始时发布消息看起来不太现实:
一个用户的消息时间线,图像来源于作者
也许自相关函数可以在检测所有可疑重复活动的用户方面提供良好的结果。
“克隆”。如果我们用更高的 K 值运行 K-Means 算法,我们也可以检测到一些“克隆”。这些聚类具有相同的时间模式和最高的轮廓系数值。例如,我们可以看到几个相似的昵称的账户,只在最后几个字符上有所不同。可能,脚本正在并行从几个账户发布消息:
几个具有相同模式的用户消息时间线,图像来源于作者
作为最后一步,我们可以看到由 t-SNE(t-分布随机邻域嵌入)算法制作的聚类可视化,这看起来相当漂亮:
t-SNE 聚类可视化,图像来源于作者
在这里我们可以看到很多较小的聚类,这些聚类没有被 K-Means(K=5)检测到。在这种情况下,尝试更高的 K 值是有意义的;也许像 DBSCAN(基于密度的空间聚类应用程序)这样的算法也会提供良好的结果。
结论
使用数据聚类,我们能够在数万条关于“#气候”的推文中找到独特的模式,这些推文由不同的用户发布。分析本身仅仅是基于推文发布的时间进行的。这在社会学或文化人类学研究中可能会很有用;例如,我们可以比较不同用户在不同话题上的在线活动,弄清楚他们发帖的频率等等。时间分析是语言无关的,因此也可以比较不同地理区域的结果,例如英语和日语用户之间的在线活动。基于时间的数据也可能对心理学或医学有用;例如,可以弄清楚人们在社交网络上花费了多少时间或他们暂停的频率。正如上面所示,发现用户“行为”中的模式不仅对研究目的有用,而且对检测机器人、克隆或发布垃圾邮件的用户等纯粹的“实际”任务也很有帮助。
可惜,并非所有的分析都成功,因为 Twitter API 没有提供时区数据。例如,看看人们是早上发消息更多还是晚上发消息更多将会很有趣,但由于没有适当的时间,无法实现;Twitter API 返回的所有消息都是 UTC 时间。但无论如何,很高兴 Twitter API 即使在免费账户下也允许我们获取大量数据。而且,显然,这篇文章中描述的思想不仅可以用于 Twitter,也可以用于其他社交网络。
如果你喜欢这个故事,可以 订阅 Medium,这样你会在我的新文章发布时收到通知,并且可以全面访问其他作者的成千上万的故事。
感谢阅读。
在 Twitter 帖子中发现时间模式:使用 Python 进行探索性数据分析(第二部分)
使用 Python 和 Pandas 进行用户行为分析
·发表于 Towards Data Science ·阅读时间 15 分钟·2023 年 6 月 12 日
--
用户时间线示例,图片由作者提供
在这篇文章的第一部分中,我分析了大约 70,000 条 Twitter 帖子的时间戳,并获得了一些有趣的结果;例如,可以检测到机器人或从克隆账户发布消息的用户。但我未能获取准确的消息时间;至少对于免费账户,Twitter API 响应没有时区,所有消息都有 UTC 时间。现在,数以百万计的人使用社交网络,对用户行为的分析不仅有趣,而且可能对社会学或心理学研究重要。例如,了解人们是在晚上、夜间还是白天发布更多消息是有趣的,但没有准确的时间就无法知道。最后,我找到了一种即使在免费 API 限制下也能很好工作的解决方法。
在本文中,我将展示从数据收集到使用 Python 和 Pandas 进行分析的完整工作流程。
方法论
我们的数据处理流程将包括几个步骤:
-
使用 Tweepy 库 收集数据。
-
加载数据并获取基本见解。
-
数据转换。我们将按用户对数据进行分组,并寻找对分析有用的特定指标。
-
分析结果。
让我们开始吧。
1. 收集数据
正如前一部分所述,我们无法获取 Twitter 消息的正确时区;所有由 Twitter API 返回的消息都具有 UTC 时间。作为一种变通方法,我决定测试三种方法:
-
我尝试使用“*”掩码获取所有消息并分析每条消息的“位置”字段。并非每个用户在 Twitter 上都指定了位置,但相当大一部分用户指定了。这个想法很好,但实际上没有效果。Twitter 是一个大型社交网络;它生成大量数据,收集一周内的所有推文是不现实的。每秒数千条消息的数量不仅对普通 PC 处理来说过大,而且也超出了免费的 Twitter 开发者账户的限制。
-
我可以使用城市名称作为请求;例如,我可以搜索所有带有“#Berlin”标签的推文。然后很容易筛选出位置为“德国”的用户,对于德国,我们知道其时区。这个想法是可行的,但问题是结果可能会有偏差。例如,带有“#Berlin”标签的消息可能是由对政治感兴趣的人或体育迷发布的。但总体来说,这种方法很有趣;通过不同的搜索查询,可能能够接触到不同类型的受众。
-
最终,我找到了一种对我有效的解决方案。我决定通过指定“*”掩码和语言代码来获取特定语言的所有消息。这显然对英语不起作用,但世界上有许多地理面积足够小的国家,可以轻松确定其公民的时区。我选择了荷兰语,因为世界上荷兰语使用者的数量并不大;这种语言主要在荷兰和比利时使用,这两个国家有相同的时区。虽然有些人可能居住在国外,也有一些荷兰语母语者在苏里南和库拉索,但这些数字也不多。
收集数据本身是简单的。代码已经在第一部分中使用过;我只指定了“*”作为查询掩码和“nl”作为语言代码。一个免费的 Twitter API 对获取历史数据有 7 天的限制。但实际上,分页的限制大约为 100,000 条消息。这算多吗?实际上,不算。大多数人可能从未意识到社交媒体上发布了多少消息。全世界只有大约 2500 万荷兰语使用者。而 100,000 是这些人在仅仅 3 小时内在 Twitter 上发布的消息数量!实际上,我需要每隔至少 2 小时运行一次代码才能获取所有的推文。
每两小时收集一次数据并不成问题;这可以轻松在云端完成,但作为一个免费的解决方案,我就使用了我的 Raspberry Pi:
Raspberry Pi 4,图片来源 en.wikipedia.org/wiki/Raspberry_Pi
Raspberry Pi 是一款小型的信用卡大小的 Linux 计算机,配备 1–8 GB 的 RAM 和 1–2 GHz 的 CPU。这些规格完全足够我们的任务,而且 Raspberry Pi 没有冷却装置,不产生噪音,功耗只有 2–5 W。因此,它是运行一到两周代码的理想选择。
我稍微修改了 Python 脚本,使其每 2 小时请求一次,并且在每个 CSV 文件的名称中添加了时间戳。在通过 SSH 登录到 Raspberry Pi 后,我可以通过使用 Linux 的 “nohup” 命令在后台运行这个脚本:
nohup python3 twit_grabs.py >/dev/null 2>&1 &
默认情况下,“nohup” 将控制台输出保存到 “nohup.out” 文件中。这个文件可能很大,所以我使用了转发到 “/dev/null” 来防止这一点。像 Cron 这样的其他解决方案也可以使用,但这个简单的命令足够完成这个任务。
这个过程在后台运行,所以我们在屏幕上看不到任何东西,但我们可以使用 “tail” 命令实时查看日志(这里的 “20230601220000” 是当前文件的名称):
tail -f -n 50 tweets_20230601220000.csv
在控制台获取推文的样子如下:
收集 Twitter 消息,作者提供的图片
如有需要,我们可以使用 “scp” 命令从 Raspberry Pi 复制新的日志:
scp pi@raspberrypi:/home/pi/Documents/Twitter/tweets_20230601220000.csv .
这里,“/home/pi/Documents/…” 是 Raspberry Pi 上的远程路径,“.” 是桌面 PC 上的当前文件夹,CSV 文件应该被复制到这里。
在我的案例中,我让 Raspberry Pi 运行了大约 10 天,这足以收集一些数据。但一般来说,时间越长越好。在为文章的前一部分准备数据时,我看到有些用户每周只发一次 Twitter 帖子;显然,需要更长的间隔才能观察到这些用户行为中的模式。
2. 加载数据
Python 脚本每 2 小时获取一次新的 Twitter 消息,并生成大量 CSV 文件作为输出。我们可以在 Pandas 中加载所有文件,并将它们合并成一个数据集:
df_tweets = []
files = glob.glob("data/*.csv")
for file_name in files:
df_tweets.append(pd.read_csv(file_name, sep=';',
usecols=['id', 'created_at', 'user_name', 'user_location', 'full_text'],
parse_dates=["created_at"],
lineterminator='\n', quoting=csv.QUOTE_NONE))
df = pd.concat(df_tweets).drop_duplicates('id').sort_values(by=['id'], ascending=True)
代码非常简单。我将每个文件加载到数据框中,然后使用 pd.concat 将所有数据框合并在一起。时间间隔是重叠的;为了避免重复记录,我使用 drop_duplicates 方法。
让我们看看我们有哪些数据:
display(df)
结果如下所示:
包含所有消息的数据框,作者提供的图片
文本和消息 ID 实际上并不重要;对于分析,我们只需要 “created_at” 字段。为了简化后续处理,让我们将日期、时间和小时提取为单独的列。我们还可以将时区偏移量添加到所有记录中:
tz_offset_hours = 2
def update_timezone(t_utc: np.datetime64):
""" Add timezone to the UTC time """
return (t_utc + np.timedelta64(tz_offset_hours, 'h')).tz_convert(None)
def get_time(dt: datetime.datetime):
""" Get time in HHMM format from the datetime """
return dt.time().replace(
second=0,
microsecond=0)
def get_date(dt: datetime.datetime):
""" Get date from the datetime """
return dt.date()
def get_datetime_hhmm(dt: datetime.datetime):
""" Get date and time in HHMM format """
return dt.to_pydatetime().replace(second=0, microsecond=0)
def get_hour(dt: datetime.datetime):
""" Get hour from the datetime """
return dt.hour
df["time_local"] = df['created_at'].map(update_timezone)
df["datetime_hhmm"] = df['time_local'].map(get_datetime_hhmm)
df["date"] = df['time_local'].map(get_date)
df["time"] = df['time_local'].map(get_time)
df["hour"] = df['time_local'].map(get_hour)
# Optionally, we can select only several days
df = df[(df['date'] >= datetime.date(2023, 5, 30)) & (df['date'] <= datetime.date(2023, 5, 31))].sort_values(by=['id'], ascending=True)
# Display
display(df)
结果如下所示:
添加列的数据框,作者提供的图片
数据加载已完成。让我们看看数据的样子。
3. 一般见解
本文旨在分析“时间”领域的模式。作为热身,让我们看看单条时间线上的所有消息。为了绘制文章中的所有图表,我将使用Bokeh库:
from bokeh.io import show, output_notebook
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models import SingleIntervalTicker, LinearAxis
from bokeh.transform import factor_cmap, factor_mark, linear_cmap
from bokeh.palettes import *
output_notebook()
def draw_summary_timeline(df_in: pd.DataFrame):
""" Group all messages by time and draw the timline """
print("All messages:", df_in.shape[0])
users_total = df_in['user_name'].unique().shape[0]
print("All users:", users_total)
days_total = df_in['date'].unique().shape[0]
print("Days total:", days_total)
print()
gr_messages = df_in.groupby(['datetime_hhmm'], as_index=False).size() # .sort_values(by=['size'], ascending=False)
gr_messages["msg_per_sec"] = gr_messages['size'].div(60)
datetime_hhmm = gr_messages['datetime_hhmm']
amount = gr_messages['msg_per_sec']
palette = RdYlBu11
p = figure(x_axis_type='datetime', width=2200, height=500,
title="Messages per second")
p.vbar(x=datetime_hhmm, top=amount, width=datetime.timedelta(seconds=50), line_color=palette[0])
p.xaxis[0].ticker.desired_num_ticks = 30
p.xgrid.grid_line_color = None
show(p)
draw_summary_timeline(df_)
在这种方法中,我按日期和时间对所有消息进行分组。我之前创建的时间戳具有“HH:MM”格式。每分钟消息数量不是一个方便的度量,所以我将所有值除以 60,以获得每秒消息数量。
结果如下:
所有推特消息,图片由作者提供
代码在树莓派上运行了大约 10 天。结果收集了 1,515,139 个独特用户发送的 6,487,433 条推特消息。但在图像中,我们可以看到一些问题。某些时间间隔缺失;可能是当时没有互联网连接。另一天也有部分缺失,我不确定是什么原因;可能是免费推特账户的优先级最低。无论如何,我们不能抱怨免费 API,我的目标是至少收集一周的数据,我已经有足够的信息了。我可以在最后删除损坏的时间间隔:
df = df[(df['date'] >= datetime.date(2023, 5, 30)) & \
(df['date'] <= datetime.date(2023, 6, 5))]
顺便提一下,时间线上的另一个点引起了我的注意;高峰发生在 6 月 4 日,当时每秒消息数量几乎翻倍。我开始对这是什么感到好奇。我们可以轻松地过滤数据框:
df_short = df[(df['datetime_hhmm'] >= datetime.datetime(2023, 6, 4, 23, 35, 0)) & \
(df['datetime_hhmm'] <= datetime.datetime(2023, 6, 4, 23, 55, 0))]
with pd.option_context('display.max_colwidth', 80):
display(df_short[["created_at", "full_text"]])
结果如下:
高峰期发布的推文,图片由作者提供
结果显示,著名足球运动员扎拉坦·伊布拉希莫维奇在 41 岁时宣布退役,这条消息引起了大量的推特转发:
推特消息时间线,图片由作者提供
正如我们所见,高峰的持续时间大约为一个小时;可能会更长,但已较晚;根据时间线,公告是在 23:35 进行的。
不过让我们回到 Pandas。为了进一步的时间分析,让我们创建两个辅助方法来绘制按一天中的时间分组的所有消息:
from bokeh.io import show
from bokeh.plotting import figure, output_file
from bokeh.models import ColumnDataSource
from bokeh.transform import linear_cmap
from bokeh.palettes import *
def draw_dataframe(p: figure, df_in: pd.DataFrame, color: str, legend_label: str):
""" Draw all messages on the 00..24 timeline """
messages_per_day = df_in.groupby(['time'], as_index=False).size()
days_total = df["date"].unique().shape[0]
msg_time = messages_per_day['time']
# Data was summarized per minute, div by 60 to get seconds
msg_count = messages_per_day['size']/(days_total*60)
source = ColumnDataSource(data=dict(xs=msg_time, ys=msg_count))
p.vbar(x='xs', top='ys', width=datetime.timedelta(seconds=50),
color=color, legend_label=legend_label, source=source)
def draw_timeline(df_filtered: pd.DataFrame, df_full: pd.DataFrame):
""" Draw timeline as a bargraph """
p = figure(width=1600, height=400, title="Messages per second", x_axis_type="datetime", x_axis_label='Time')
palette = RdYlBu11
draw_dataframe(p, df_full, color=palette[0], legend_label="All values")
if df_filtered is not None:
draw_dataframe(p, df_filtered, color=palette[1], legend_label="Filtered values")
p.xgrid.grid_line_color = None
p.x_range.start = 0
p.x_range.end = datetime.time(23, 59, 59)
p.xaxis.ticker.desired_num_ticks = 24
p.toolbar_location = None
show(p)
这样我们可以看到所有消息在单个 24 小时时间线上的分布:
draw_timeline(df_filtered=None, df_full=df)
可选的“df_filtered”参数将在后续使用。结果如下:
每日消息,图片由作者提供
我们可以清晰地看到昼夜差异,因此我假设大多数荷兰消息来自相同的时区是正确的。
我们还可以绘制单个用户的时间线。我在之前的部分已经使用了这种方法。为了方便那些可能将本文作为教程的读者,我会将代码也放在这里:
def draw_user_timeline(df_in: pd.DataFrame, user_name: str):
""" Draw cumulative messages time for specific user """
df_u = df_in[df_in["user_name"] == user_name]
# Group messages by time of the day
messages_per_day = df_u.groupby(['time'], as_index=False).size()
msg_time = messages_per_day['time']
msg_count = messages_per_day['size']
# Draw
p = figure(x_axis_type='datetime', width=1600, height=150,
title=f"Cumulative tweets timeline: {name} ({sum(msg_count)} messages)")
p.vbar(x=msg_time, top=msg_count, width=datetime.timedelta(seconds=30), line_color='black')
p.xaxis[0].ticker.desired_num_ticks = 30
p.xgrid.grid_line_color = None
p.toolbar_location = None
p.x_range.start = datetime.time(0,0,0)
p.x_range.end = datetime.time(23,59,0)
p.y_range.start = 0
p.y_range.end = 1
p.yaxis.major_tick_line_color = None
p.yaxis.minor_tick_line_color = None
p.yaxis.major_label_text_color = None
show(p)
draw_user_timeline(df, user_name="Ell_____")
结果如下:
单个用户的消息时间线,图片由作者提供
4. 数据转换
在上一步中,我们获得了所有用户消息的“原始”数据框。我们将找到每日模式,因此作为输入数据,让我们获取按小时分组并计算的每用户消息数量:
gr_messages_per_user = df.groupby(['user_name', 'hour'], as_index=True).size()
display(gr_messages_per_user)
结果如下:
作为提醒,我使用了 7 天的数据。在这个例子中,我们可以看到在此时间段内,用户在早上 7 点发布了 4 条消息,在早上 8 点发布了 1 条消息,在下午 5 点发布了 3 条消息,等等。
对于分析,我决定使用三个指标:
-
用户每天进行 Twitter 发布的“繁忙小时”总数(在上一个示例中,数量是 5)。
-
每个用户的消息总数(在上一个示例中,数量是 20)。
-
一个包含 24 个数字的数组,表示按小时分组的消息数量。作为一个重要步骤,我还会将数组和标准化为 100%。
输出将是一个按用户名分组的新数据框。这个方法完成了所有的计算:
def get_user_hours_dataframe(df_in: pd.DataFrame):
""" Get new dataframe of users """
busy_hours = []
messages = []
hour_vectors = []
vectors_per_hour = [[] for _ in range(24)]
gr_messages_per_user = df_in.groupby(['user_name', 'hour'], as_index=True).size()
users = gr_messages_per_user.index.get_level_values('user_name').unique().values
for ind, user in enumerate(users):
if ind % 50000 == 0:
print(f"Processing {ind} of {users.shape[0]}")
hours_all = [0]*24
for hr, value in gr_messages_per_user[user].items():
hours_all[hr] = value
busy_hours.append(get_busy_hours(hours_all))
messages.append(sum(hours_all))
hour_vectors.append(np.array(hours_all))
hours_normalized = get_hours_normalized(hours_all)
for hr in range(24):
vectors_per_hour[hr].append(hours_normalized[hr])
print("Making the dataframe...")
cdf = pd.DataFrame({
"user_name": users,
"messages": messages,
"hours": hour_vectors,
"busy_hours": busy_hours
})
# Add hour columns to the dataframe
for hr in range(24):
cdf[str(hr)] = vectors_per_hour[hr]
return cdf.sort_values(by=['messages'], ascending=False)
def get_hours_normalized(hours_all: List) -> np.array:
""" Normalize all values in list to 100% total sum"""
a = np.array(hours_all)
return (100*a/linalg.norm(a, ord=1)).astype(int)
df_users = get_user_hours_dataframe(df)
with pd.option_context('display.max_colwidth', None):
display(df_users)
结果如下:
数据指标,按用户分组,作者提供的图片
现在我们有一个包含所有指标的数据框,准备开始处理这些数据。
5. 分析
在最后一步中,我们将包含所有 Twitter 消息的“原始”数据框转换为按用户分组的数据。这个数据框实际上更有用。作为热身,让我们从简单的开始。让我们获取每个用户的消息数量。数据框已经排序,我们可以轻松看到发布最大数量消息的“前 5 名”用户:
display(df_users[:5])
指标数据框,按用户分组,作者提供的图片
让我们还找到百分位数:
> print(df_users["messages"].quantile([0.05, 0.1, 0.5, 0.9, 0.95]))
0.05 1.0
0.10 1.0
0.50 1.0
0.90 4.0
0.95 10.0
结果很有趣。这些数据是在 7 天内收集的。数据框中有 1,198,067 个唯一用户在此期间发布了至少一条消息。而 90 百分位数仅为 4,这意味着 90%的用户在这一周内仅发布了 4 条消息。与发布超过 5000 条推文的顶级用户相比,差异很大!但正如在第一部分讨论的那样,一些“顶级用户”可能是机器人。我们可以通过使用每小时消息数量轻松验证这一点。我已经有了按小时分组并标准化到 100%的消息数量。让我们找出连续发布消息而没有任何延迟的用户。为此,我们只需筛选出每小时发布 100/24 = 4%消息的用户:
df_users_filtered = df_users.copy()
for p in range(24):
df_users_filtered = df_users_filtered[(df_users_filtered[str(p)] >= 2) & \
(df_users_filtered[str(p)] <= 5)]
display(df_users_filtered)
for user_name in df_users_filtered["user_name"].values:
draw_user_timeline(df, user_name)
这个数字可能不完全是 4%,因此我使用了 2..5%作为筛选范围。结果发现有 28 个“用户”每小时发布相同数量的消息:
发布等间隔消息的“用户”,作者提供的图片
在前一部分,我已经使用聚类算法检测到了一些机器人。在这里,我们可以看到即使使用更简单的方法,我们也可以获得类似的结果。
让我们进入更有趣的部分,按活动时间分组用户。由于每小时的消息总量被标准化为 100%,我们可以进行相当复杂的请求。例如,让我们添加新的列“早晨”、“白天”、“晚上”和“夜晚”:
df_users["night"] = df_users["23"] + df_users["0"] + df_users["1"] + df_users["2"] + df_users["3"] + df_users["4"] + df_users["5"] + df_users["6"]
df_users["morning"] = df_users["7"] + df_users["8"] + df_users["9"] + df_users["10"]
df_users["day"] = df_users["11"] + df_users["12"] + df_users["13"] + df_users["14"] + df_users["15"] + df_users["16"] + df_users["17"] + df_users["18"]
df_users["evening"] = df_users["19"] + df_users["20"] + df_users["21"] + df_users["22"]
对于分析,我将只使用那些发了超过 10 条消息的用户:
df_users_ = df_users[(df_users['messages'] > 10)]
df_ = df[df["user_name"].isin(df_users_["user_name"])]
当然,10 不是统计上显著的数字。这只是一个概念证明,对于真正的研究,建议在更长的时间间隔内收集数据。
无论如何,结果都很有趣。例如,我们可以找到大部分消息都是早晨发出的用户,只需一行代码。我们还可以获取所有这些消息并在时间线上绘制出来:
df_users_filtered = df_users_[df_users_['morning'] >= 50]
print(f"Users: {100*df_users_filtered.shape[0]/df_users_.shape[0]}%")
df_filtered = df_[df_["user_name"].isin(df_users_filtered["user_name"])]
draw_timeline(df_filtered, df_)
结果如下:
早晨发推文的用户,图片由作者提供
有趣的是,这个数字只有大约 3%。作为对比,46%的活跃用户在白天发送超过 50%的消息:
白天发推文的用户,图片由作者提供
我们可以提出其他请求;例如,让我们找出80% 的消息都是在晚上发送的用户:
df_users_filtered = df_users_[df_users_['evening'] >= 80]
结果如下:
晚上发推文的用户,图片由作者提供
我们还可以显示一些用户的时间线以验证结果:
for user_name in df_users_filtered[:5]["user_name"].values:
draw_user_timeline(df_, user_name)
输出如下:
选择用户的时间线,图片由作者提供
结果可能很有趣;例如,用户“rhod***”几乎所有消息都集中在 19:00 之后发送。
我必须再次强调,这些结果不是最终的。我仅分析了在一周内发布了超过 10 条推文的较为活跃的用户。但还有大量用户发消息较少,要获得更多见解,数据应在几周甚至几个月内收集。
结论
在这篇文章中,我们能够获取所有用特定语言发布的 Twitter 消息——在我们的例子中是荷兰语。这种语言主要用于荷兰和比利时,这两个国家彼此接近。这使我们能够了解用户的时区,但遗憾的是,我无法从 Twitter API 中获得这一信息,至少使用免费账户是无法做到的。通过分析消息时间戳,我们可以获得很多有趣的信息;例如,可以找出用户是在早晨、工作时间还是晚上更活跃。发现用户行为的时间模式对于心理学、文化人类学甚至医学都可能有用。数百万的人在使用社交网络,了解这如何影响我们的生活、工作节奏或睡眠是很有趣的。正如文章所示,分析这种行为可以通过简单的请求完成,这实际上并不比学校数学更难。
看到社交网络可以存储如此大量的数据也很有趣。我想大多数人从未考虑过发布了多少消息。即使是相对较小的荷兰语社区(全球约 2500 万母语者),每秒钟也能生成超过 10 条推文。对于这篇文章,分析了来自 1,515,139 用户的 6,487,433 条 Twitter 消息,这些消息仅仅是 10 天内发布的!对于像德国这样的大国来说,获取所有消息可能会超出免费的 Twitter 开发账户的限制。在这种情况下,可以考虑结合不同的请求查询,并按用户位置进行过滤。
无论如何,社交网络是一个关于我们的有趣信息来源,我祝愿读者在自己的实验中好运。那些感兴趣的人也欢迎阅读关于使用 K-Means 算法对 Twitter 用户进行聚类的第一部分。另外,Twitter 帖子也进行了 NLP 分析。
如果你喜欢这个故事,可以订阅Medium,这样你将收到我新文章发布的通知,并可以全面访问其他作者的数千个故事。
感谢阅读。
使用机器视觉找到边境通行的最快车道
优化边境通行的目标检测与跟踪
·
关注 发表在 Towards Data Science · 12 分钟阅读·2023 年 2 月 16 日
--
从“Blace”边境通行相机(公有领域)生成的图像
穿越边境可以是任何公路旅行中的激动人心的部分,但在边境通行处排长队的挫败感会迅速消磨这种兴奋感。如果我们能确定最快的车道并相应地调整位置呢?感谢机器视觉的进步,这现在已经成为可能。通过利用 OpenCV 和 YOLOv3(一个深度学习算法),现在可以实时检测和追踪移动的车辆。在这篇文章中,我们将探讨如何使用这些技术帮助人们节省时间,避免在边境通行处长时间排队的压力。
内容
-
问题概述
-
数据
-
目标检测
– 汽车检测
– 车道检测
-
目标追踪
-
协调所有内容
-
限制
-
未来工作
问题概述
这个任务的主要想法是处理来自边境摄像头实时流的原始图像数据,以检测和追踪移动的车辆。这个问题可以分为几个步骤:
-
从边境摄像头的直播中获取原始图像数据
-
预处理图像
-
应用算法来寻找车辆的位置
-
确定这些车辆的车道
-
应用算法来确定车辆的速度(从而确定每条车道的速度)
所有这些步骤都通过 docker-compose 结合起来,以便在多个边境视频流上运行解决方案。
数据
视频数据来源于对公众开放的边境摄像头。这些解决方案可以应用于任何边境/收费站摄像头的实时流媒体 (.m3u8),并且可以很容易地适应其他形式的视频。几乎所有国家都有某种服务,让用户能够查看边境通行的实时情况。在这个具体的案例中,我使用了我国家摄像头的实时视频流。你可以通过这个 链接 找到这些视频流。
目标检测
在运行任何算法之前,我们需要加载视频流并对图像进行预处理。这可以通过 OpenCV2 轻松完成。我创建了一个将图像调整为 416x416 像素的函数(这是我使用的 YOLOv3 模型实现所要求的格式)。
def preprocess(image):
resized_image = cv2.resize(image, (416, 416))
blob = cv2.dnn.blobFromImage(resized_image, 1 / 255, (416, 416), swapRB=True, crop=False)
return resized_image, blob
在一个循环中,从流媒体中加载图像,并使用上述函数对其进行预处理。所有接下来的更改也将在这个循环中完成。此外,我检查是否与前一帧有所变化,因为如果没有变化,重新处理图像是没有必要的。
old_frame = None
while True:
# read the stream and get the current image
cap = cv2.VideoCapture(BORDER_URL.m3u8)
ret, frame = cap.read()
if not ret:
break
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# resize the image to correct formant
image, blob = preprocess(frame)
# check if the image is the same as the previous one by calculating the mean squared error between the two
if old_frame is not None:
mse = np.mean((image - old_frame) ** 2)
if mse < 0.001:
old_frame = image
continue
汽车检测
YOLOv3 架构用于检测对象(感谢原始 YOLOv3 论文)
现在,我们已经准备好在图像中检测车辆。我使用了 YOLOv3 来获取模型训练过的所有类别的边界框,但只保留了车辆的边界框。加载此模型需要权重和配置文件,你可以从官方网站下载。这一函数返回边界框、模型对检测对象的置信度、对象的类别(如汽车、卡车、行人等),以及每个边界框的中心点。
def detect_cars(image, blob):
net = cv2.dnn.readNet("./yolov3.weights", "./yolov3.cfg")
net.setInput(blob)
layer_names = net.getLayerNames()
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]
predictions = net.forward(output_layers)
boxes = []
confidences = []
class_ids = []
centers = []
for prediction in predictions:
for detection in prediction:
scores = detection[5:]
class_id = int(np.argmax(scores))
confidence = float(scores[class_id])
# Filter out weak detections
if confidence > 0.5:
# Get detection coordinates
x, y, w, h = (detection[0:4] * np.array(
[image.shape[1], image.shape[0], image.shape[1], image.shape[0]])).astype("int")
x = int(x - w / 2)
y = int(y - h / 2)
center_x = int(x + w // 2)
center_y = int(y + h // 2)
centers.append((center_x, center_y))
boxes.append([int(x), int(y), int(x + w), int(y + h)])
confidences.append(float(confidence))
class_ids.append(class_id)
return boxes, confidences, class_ids, centers
算法在对“Blace”边境检查摄像头(公共领域)应用 NMS 之前的输出
我们有了关于边界框的所有信息,但是当我们运行应用程序时,发现有很多重叠的框需要去除。为了解决这个问题,我使用了非极大值抑制(NMS)。非极大值抑制(NMS)是一种后处理算法,用于对象检测任务,特别是用于去除同一对象的冗余或重叠的边界框。
NMS 用于过滤冗余的边界框,只保留最合适的框。NMS 的基本思路是首先根据检测置信度得分(即边界框包含对象的概率)对边界框进行排序。从置信度得分最高的边界框开始,NMS 抑制所有交并比(IoU)值大于某个阈值(例如 0.5)的重叠边界框。
以下函数执行此算法。它的参数包括边界框、边界框的置信度和我们想要过滤掉重复项的阈值。它返回我们需要保留的框的 ID。
def NMS(boxes, confidences, threshold):
if len(boxes) == 0:
return []
boxes = np.array(boxes)
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
scores = confidences
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= threshold)[0]
order = order[inds + 1]
return keep
使用这个算法后,每辆车只保留一个边界框。你可能需要调整阈值以找到适合你的设置。
车道检测
虽然有一些车道检测算法可能对这个任务有效,但它们可能不是最有效的选择。由于摄像头是固定的,车道位置不会变化,使用这些算法会浪费处理时间。为了解决这个问题,我将车道位置保存到了一个 JSON 文件中,作为点的列表。然而,在确定车道速度之前,还面临一个额外的挑战:识别每辆车所在的车道。
为此,我创建了以下函数,根据车辆的 x 坐标找到每辆车所在的位置。它返回车道的索引。如果车辆在车道之外,它返回-1。
def get_x_at_y(line, y):
loc = -1
for segment in range(len(line)-1):
if line[segment][1] <= y <= line[segment + 1][1] or line[segment][1] >= y >= line[segment + 1][1]:
loc = segment
if loc == -1:
return None
slope = (line[segment+1][1] - line[segment][1]) / (line[segment+1][0]-line[segment][0])
return (y-line[segment+1][1])/slope + line[segment+1][0]
def track_lanes(car_centers, image, lanes):
if len(car_centers) < 2:
return []
ccs_with_pos = []
for cc in car_centers:
xs = []
for lane in lanes:
xs.append(get_x_at_y(lane, cc[1]))
dists = []
if None in xs:
ccs_with_pos.append((cc, -1))
continue
for x in xs:
dists.append(x-cc[0])
if dists[0]>0 or dists[-1]<0:
ccs_with_pos.append((cc, -1))
continue
# check if x value of the car is between each two lines
for i in range(len(dists)-1):
if dists[i] < 0 < dists[i + 1]:
ccs_with_pos.append((cc, i))
break
return ccs_with_pos
我们已经准备好通过实施对象跟踪来查看每个车道的变化率。
对象跟踪
目标跟踪是定位视频流中一个或多个特定物体并跟踪其移动过程。目标跟踪的目标是在视频的连续帧中保持物体的身份,尽管物体的位置、方向、大小和外观可能发生变化。虽然存在许多目标跟踪算法,但对于这个特定的使用案例,我决定使用光流法,因为它在较低帧率的视频流中效果相当好。
光流法是一种技术,通过分析相邻像素在连续视频帧中的强度变化来确定场景中物体的运动方向和速度。光流算法依赖于一个假设,即一个帧中的像素亮度与下一个帧中对应像素的亮度相同,从而允许计算物体在帧之间的位移。
这个算法要复杂得多,所以我不会详细解释。作为参数,它接收图像和前一图像、YOLOv3 模型中的汽车中心、车道以确定是否有运动发生在每个车道中,以及车道中的汽车以查看运动是否是由车辆运动或其他因素引起的。它返回每个车道的速度列表。
def optical_flow(image, prev_image, features_to_track, lanes, cars_in_lanes):
if len(cars_in_lanes) == 0:
return list([-1 for el in range(len(lanes))])
feature_params = dict(maxCorners=100,
qualityLevel=0.3,
minDistance=20,
blockSize=7)
lk_params = dict(winSize=(30, 30),
maxLevel=5,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
prev_gray = cv2.convertScaleAbs(prev_image)
prev_gray = cv2.cvtColor(prev_gray, cv2.COLOR_BGR2GRAY)
p0 = np.array(features_to_track).reshape((-1, 1, 2))
p0 = np.float32(p0)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Compute optical flow
p1, st, err = cv2.calcOpticalFlowPyrLK(prev_gray, gray, p0, None, **lk_params)
# Filter good points
good_new = p1[st == 1]
good_prev = p0[st == 1]
# Find the speed of each lane
lane_speeds = list([0 for el in range(len(lanes))])
for i, (new, prev) in enumerate(zip(good_new, good_prev)):
x_new, y_new = new.ravel()
x_prev, y_prev = prev.ravel()
xs_new = []
xs_old = []
for lane in lanes:
xs_new.append(get_x_at_y(lane, y_new))
xs_old.append(get_x_at_y(lane, y_prev))
dists_new = []
dists_old = []
if None in xs_new or None in xs_old:
continue
for x in xs_new:
dists_new.append(x - x_new)
for x in xs_old:
dists_old.append(x - x_prev)
if dists_new[0] > 0 or dists_new[-1] < 0 or dists_old[0] > 0 or dists_old[-1] < 0:
continue
lane_new = None
lane_old = None
for j in range(len(dists_new) - 1):
if dists_new[j] < 0 < dists_new[j + 1]:
lane_new = j
break
for j in range(len(dists_old) - 1):
if dists_old[j] < 0 < dists_old[j + 1]:
lane_old = j
break
lane_speeds[lane_new] += math.dist([x_new, y_new],[x_prev, y_prev])
for i, ls in enumerate(lane_speeds):
cnt = 0
for car in cars_in_lanes:
if car[1] == i:
cnt += 1
if cnt == 0:
lane_speeds[i] = -1
continue
lane_speeds[i] = lane_speeds[i]/cnt
return lane_speeds
光流算法的输出被保存并与前一帧的输出进行比较。如果每个车道的光流值发生显著变化,则记录为 1 个移动点。这在以下函数中实现。
def calc_movement(new_movement, old_movement, sum_movement):
for i in range(len(new_movement)):
if new_movement[i] > old_movement[i] + 1:
sum_movement[i] += 1
return sum_movement
现在一切都已实现,我们可以可视化结果。这个功能显示带有每辆车的边界框的图像、交通车道,并在控制台打印车道的速度。
def visualize(boxes, centers, class_ids, keep, image, colors, lanes, ccs_with_pos):
for lane in lanes:
cv2.polylines(image, [np.array(lane).reshape((-1, 1, 2))], False, (0,0,255), 2)
for i, (box, c, ci) in enumerate(zip(boxes, centers, class_ids)):
if i not in keep:
continue
class_list = ['car', 'motorbike', 'bus', 'truck', 'person']
if classes[ci] in class_list:
color = colors[class_list.index(classes[ci])]
thickness = 2
x, y, w, h = box
cv2.rectangle(image, (x, y), (w, h), color, thickness)
if ccs_with_pos is not None:
prev_vehs = list([None for el in range(len(lanes))])
for i in range(416, 0, -1):
for car in ccs_with_pos:
if car[0][1] == i and car[1] != -1:
if prev_vehs[car[1]] is None:
prev_vehs[car[1]] = car
cv2.putText(image, 'lane ' + str(car[1]+1), (car[0][0]-10, car[0][1]+10), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (255, 0, 0), 2, cv2.LINE_AA)
else:
cv2.line(image, car[0], prev_vehs[car[1]][0], (255,0,0), 2)
prev_vehs[car[1]] = car
cv2.imshow("image", image)
在主循环中运行一切后,我们得到以下视频。程序输出显示第二车道(车道 2)的移动最多,因此速度最快。
“Blace”边界穿越的约 3 分钟加速 GIF(公共领域)
协调整个过程
尽管这个算法对于检测和跟踪单一边界穿越中的车辆已经完整,我希望将解决方案扩展到多个边界穿越,同时仍然保存速度结果。为实现这一目标,我决定将每个边界摄像头脚本作为一个独立的容器运行,该容器在 docker-compose 中进行通信。由于为每个摄像头运行单独的 YOLOv3 模型效率低下,我创建了一个带有 flask 应用程序的独立容器,用于服务其他容器。然而,为了确保多个请求不会同时使用相同的神经网络,重要的是要采用信号量或其他锁定机制。
此外,我开发了一个 .Net 网络 API,用于将每条车道的速度和汽车保存到数据库中。由于篇幅限制,这里无法包含完整的代码,但你可以在我的 GitHub 仓库 中找到它。通过这种实现,该解决方案可以部署到多个边境检查点,生成的数据可以高效地收集和分析,为不同地点的交通模式和拥堵情况提供有价值的见解。
version: '3'
services:
api:
container_name: api
build:
context: ./api
dockerfile: Dockerfile
image: api:latest
ports:
- 80:80
restart: always
yolo:
build: ./yolo_detector
command: python script.py
container_name: yolo
ports:
- 5001:5001
restart: always
script0:
build: ./image_processing
command: python count.py 0
container_name: script0
restart: always
script1:
build: ./image_processing
command: python count.py 1
container_name: script1
restart: always
...
限制
尽管我们的算法已被证明非常有效,但重要的是要承认它的局限性。首先,我使用的目标检测模型 YOLOv3 在低光条件下(如夜间)的准确性较差。因此,检测到的车辆数量会显著减少,这可能导致对交通量的错误判断。此外,该模型在长距离检测物体时存在困难,这意味着在长队中可以检测到的车辆数量有限。
此外,考虑到边境检查点的设计可以有很大的差异也很重要。虽然大多数边境检查点可能遵循类似的布局,但也有一些具有更复杂特征,例如弯曲车道和额外障碍物。这些变化可能会导致算法在某些情况下无法使用。值得注意的是,进一步的开发和优化可能会解决这些限制,但目前在实施我们的解决方案时必须考虑这些因素。
“Tabanovce” 检查点(公共领域)中无法使用车道速度算法的示例边境检查点。
未来工作
虽然这个解决方案提供了一种有效的方式来确定边境检查点的车辆速度和交通拥堵情况,但仍有改进的空间。未来工作的一个方向是探索其他目标检测模型,看看它们在低光条件下或在检测远距离物体时是否能提供更好的准确性。
此外,调查在布局更复杂的边境检查点实施该算法的可行性也可能是有益的。这可能涉及开发新的算法,以应对弯曲车道、额外障碍物或其他特征带来的挑战。
此外,目前的解决方案可以扩展到结合机器学习技术,以适应随时间变化的交通模式。通过对长期收集的数据进行训练,模型可能会学会根据交通状况调整其参数,从而随着时间的推移提高其准确性和可靠性。
最后,可以考虑整合来自其他来源的数据,如社交媒体或交通摄像头,以提供对边境检查点交通模式的更全面理解。通过结合来自各种来源的数据,可能会发现交通模式与外部因素(如天气或公共事件)之间的关联,为交通管理和未来规划提供有价值的见解。这些数据可以帮助人们更好地规划出行,设置最少预期交通的出发时间。
结论
总之,通过使用机器视觉算法,我们已经开发出了一种有效的解决方案来确定边境检查点的车辆速度和交通流量。你可以在GitHub上试验代码,并尝试查看你附近边境检查点的结果。感谢你的阅读!
寻找你梦想中的人工智能硕士项目
基于价值观的项目选择和网络建设方法。
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 6 月 1 日
--
摄影师 Mikael Kristenson 摄于 Unsplash
去年我申请并被接受到 12 个人工智能研究生项目,包括韩国先进科学技术学院(KAIST)、哥伦比亚大学和伦敦大学学院(UCL)。
在研究申请研究生项目的最佳实践时,我发现一些资源主张仅根据大学的纸面形象来选择你的项目。虽然这可以很好地反映学校的整体地位,但这种方法没有考虑到包括你希望申请的项目类型在内的许多因素。你希望申请什么类型的项目非常重要。
我的解决方案是?基于价值观的决策和积极的网络建设。
在本文中,我将概述我如何找到理想的人工智能研究生项目,并与其教师建立联系,以在招生周期开始之前获得研究职位。按照这里概述的过程,我相信你也能找到理想的人工智能研究生项目。
基于价值观的项目选择
基于价值观的决策有助于简化任何决策过程。决定申请哪些研究生项目可以帮助你快速有效地筛选众多项目。
在你开始寻找项目之前,你首先需要确定与你攻读硕士学位相关的价值观。确定我的价值观在帮助我找到与个人和职业目标一致的完美项目中发挥了重要作用。
尽管每个人的价值观不同,但有些价值观更为普遍。这些包括财务稳定性、时间承诺和社交生活等。如果你不确定从哪里开始,我建议你阅读这篇文章作为起点。
我确定了自己的价值观:职业流动性、个人兴趣、教育、动力、地点和发展机会。虽然这些因素在我的最终决定中都很重要,但有三个对我来说尤为关键:动力、教育和地点。
动力
如果你正在阅读这篇文章,那么你很可能已经找到了你的why并决定研究生院适合你。即便如此,在你找到硕士项目之前,你应该先确定你去研究生院的动力。
你计划攻读博士学位吗?你想要更高的起薪?更好的工作?只是更多的课堂时间?没有“好”或“不好”的研究生院动机,但缺乏动机是你应该警惕的。
我去研究生院的动力是增强我对人工智能的知识,并了解它如何在应对气候变化的斗争中得到应用。
教育
人工智能研究生项目通常分为三大类:MSci、MSc 和 MRes。尽管这三者都将教授与人工智能相关的相似概念,但它们也会教你不同的知识应用方式。下表展示了这些项目的高级别分类。
MSci、MSc 和 MRes 的比较。图像由作者提供。
一个重要的事情是要注意 MSc 和 MSci 学位之间的区别。有时被称为“综合硕士”的 MSci 是一个扩展的本科学位,而 MSc 是一个独立的硕士项目。
最后,我决定寻找一个人工智能硕士课程,因为这将为我提供在研究和应用人工智能方面的丰富经验。
位置
现在你已经知道你想学习什么,但现在你应该考虑你想在哪里学习。你想去你的母校吗?还是去国外的全新大学?
你对这些问题的回答完全取决于你的地理流动性。如果由于个人、财务或家庭原因无法远离目前的位置,那么你应该将搜索范围限制在本地的课程。或者,如果你可以灵活选择地理位置,那么你可以将搜索范围扩展到你所在国家的其他地方,甚至是国际上。
由于我可以灵活地选择地理位置,我决定同时考虑美国和国外的课程。
背景研究
现在,你已经确定了基于价值观的方法,是时候用它们来找到你梦寐以求的人工智能硕士课程了。
寻找学校
目前为止,用于识别认证大学的最佳计算机科学工具是CSRankings.com。CSRankings 根据教职员工在计算机科学及相关领域的研究活跃程度对大学进行排名。在这个列表上排名高是行业相关性的良好指标。
为了筛选我的搜索结果,我利用上述数值来匹配我的个人标准,同时将所有领域菜单限制在“人工智能”和“机器学习”之间。你可以查看搜索结果这里。
虽然数量有所不同,但有一个来源建议找到四到六个课程申请。然而,如果你有额外的时间和额外的资金来支付申请费用,你可以申请任意数量的课程。在我的情况下,我花了两个月的时间研究和申请 2022 年夏天的各种课程,这就是为什么我选择申请大量的课程。
浏览列表时,我确定了我想要了解的课程。其中,我最喜欢的三个是 KAIST(排名第 4)、哥伦比亚大学(排名第 41)和伦敦大学学院(排名第 59)。
寻找一个课程
一旦确定了学校的列表,下一步就是访问该学校的网站,查找他们的研究生课程列表。
我的首选是 KAIST,所以我浏览了工程学院的网站,找到了他们的课程列表。然后我选择了对我最相关的课程 —— 人工智能硕士学位。
KAIST 工程学院研究生课程。图片由作者提供。
你应该为列表中的每所学校重复此过程,并注意任何其他激起你兴趣的项目。例如,UCL 有两个我感兴趣的项目;MSc Data Science 和 MSc AI for Sustainable Development。然而,一些大学限制研究生申请人每个周期只能提交一个申请。在这种情况下,了解大学对多个申请的政策就显得尤为重要。
项目特定考虑
现在你已经找到一个符合你价值观的项目,下一步是查看项目特定信息,以确保它适合你。
最终,这些考虑因素与你选择硕士项目时的价值观同样重要,因此在申请任何项目之前进行尽职调查是个好主意。
有疑问时,请问! 联系学校的招生办公室通常是快速回答任何与项目相关问题的好方法。
重要的项目信息
寻找的最重要的事情之一是财务支持、申请截止日期和住房情况等。幸运的是,大多数大学已经为潜在学生编制了这些信息,以“申请指南”或“学生手册”的形式。
在查看上述项目页面时,我找到了KAIST 申请指南,其中详细列出了此处提到的所有内容。此外,一些学校,如哥伦比亚大学,将所有相关信息列在他们的网站上。
资金筹措方式
在选择项目时,最值得注意的考虑之一是资金筹措方式。一些学校提供竞争性学费奖学金和生活补助包,而其他学校则没有。在个人情况的财务可行性方面审查学校是个好主意。
如果你计划自掏腰包支付学费,那么在选择项目时你会有更多的灵活性。然而,鉴于大多数人不会自己支付项目费用,确定资金机会对于找到合适的项目至关重要。这也是询问雇主是否有任何为员工高等教育提供资金的计划的好时机。
在美国有许多由私人机构和政府支持的资金机会。然而,其他国家也提供类似的项目,比如韩国的 GKS和德国的 DAAD。
一些大学还提供基于需求的资助机会,帮助那些无法自费完成硕士学位的人。这里 是来自哥伦比亚大学的一个例子。
对于我来说,KAIST 提供了一个具有竞争力的经济援助方案,这在我的决策中占了很大的比重。大多数国际学生在 KAIST 可以免学费,并获得每月津贴以帮助生活开销。
网络建立
一旦你缩小了潜在项目的范围,主动与教师建立联系可以给你宝贵的见解,并帮助你调整申请材料。在这方面,建立网络可以采取多种形式,包括参加信息会议、参观校园或联系教师。在我的搜索中,我选择了后者。
通过这样做,我不仅能够与教授建立联系,还能准确了解他们在寻找什么样的学生,从而让我可以根据每个特定项目调整我的申请。虽然并非所有项目都可行,但我的目标是每所学校联系两位教师。
当我与教师网络建立联系时,我希望了解该项目超出我在线找到的内容。具体来说,每封我发送的邮件中,我想回答三个问题:
-
我可以做什么来让我的申请脱颖而出?
-
学生有哪些研究机会?
-
这位教授是否能在我的项目期间提供指导?
我浏览了我感兴趣的项目的教师页面,挑选了几位研究兴趣与我相符的教授。然后,我创建了一个模板电子邮件和一个电子表格,用于跟踪我联系了谁和何时联系。
我(匿名化的)研究生网络联系列表。图片由作者提供。
像这样主动的网络建立既费时又有益。为了加快这个过程,我将我的模板电子邮件设计成只需要关于每位教师的三条信息:他们的名字、研究兴趣以及他们所在的学校(例如计算机学院、人工智能学院)。这些信息用于填写我的模板电子邮件,并连同我的简历和非官方成绩单一起发送给他们。
您好,教授 [姓名],
我叫 Simon Aytes,是一名潜在的研究生,来自 [大学]。目前,我正在寻找 [学期]* 开始的全日制项目。*
我在研究 [UNIVERSITY] 的不同项目时,在教职员工页面上发现了您。我很乐意与您进一步探讨关于 [UNIVERSITY] 和 [SPECIFIC RESEARCH OR TOPIC] 的内容。我可以根据您的时间安排来进行讨论。附件中附有我的简历和成绩单供您参考。
期待您的回复。感谢您的时间。
*致意,
Simon Aytes*
这个模板可以进一步修改,以包含与教职员工有关的研究、工作经验或活动补助的信息。
通过遵循这一过程,我给我申请的 12 个项目中的 30 多位教授发送了电子邮件。其中,我通过电子邮件与 15 位教授建立了联系,并与更多教授进行了虚拟的“咖啡聊天”。正是通过这一网络努力,我成功地在我首选的大学获得了导师和研究职位。
最后思考
最后,基于价值观的决策和积极的社交网络仅仅是整个难题的一部分。决定您是否被感兴趣的项目录取的因素有很多。工作经验、GPA 和标准化考试成绩(如 GRE)等因素都严重影响您申请项目的资格。
找到完美的人工智能研究生项目需要自我反思、研究和积极的社交网络。通过遵循本文概述的过程,您可以在正式申请过程中为自己赢得一个强有力的开端。
Simon Aytes 目前是首尔韩国的 KAIST 人工智能研究院研究生 。他曾与 NASA、Penta Group 和哥伦比亚大学等组织合作过。
想了解更多吗?访问他的 网站 或通过 LinkedIn* 与他联系吧!*
用 Python 微调大型语言模型
原文:
towardsdatascience.com/fine-tune-a-large-language-model-with-python-b1c09dbc58b2
Manouchehr Hejazi 的照片,来源于 Unsplash
学习如何在自定义数据集上从头开始微调 BERT
·发布在 Towards Data Science ·4 分钟阅读·2023 年 4 月 18 日
--
在这篇文章中,我们将处理使用 PyTorch 的BERT 情感分类微调。BERT 是一个大型语言模型,提供了受欢迎程度与模型大小之间的良好平衡,可以使用简单的 GPU进行微调。我们可以从 Hugging Face (HF) 下载预训练的 BERT,因此无需从头开始训练。特别是,我们将使用名为Distil-BERT的精简(更小)版本。
Distil-BERT 在生产中广泛使用,因为它比 BERT uncased少了 40% 的参数。它运行快 60% 并保持 95% 的性能在 GLUE 语言理解基准中。
我们首先安装所有必要的库。第一行是捕获安装输出,并保持你的笔记本整洁。
我将使用 Deepnote 来运行本文中的代码,但如果你更喜欢,也可以使用 Google Colab。
你还可以使用以下代码行检查你正在使用的库的版本。
现在你需要指定一些常规设置,包括训练轮次和设备硬件。我们设置了一个固定的随机种子,以帮助实验的可重复性。
加载 IMDb 电影评论数据集
让我们来看一下如何准备和标记 IMDb 电影评论数据集,并微调 Distilled BERT。获取压缩数据并解压缩它。
和往常一样,我们需要将数据拆分为训练集、验证集和测试集。
标记数据集
让我们使用从预训练模型类继承的分词器实现将文本分词为单个单词标记。
分词化用于自然语言处理,将段落和句子分割成更小的单位,以便更容易赋予其意义。
通过 Hugging Face,你将总是能找到与每个模型相关联的分词器。如果你不是在研究或实验分词器,通常更推荐使用标准分词器。
填充是一种通过向较短的句子添加特殊填充标记来确保张量是矩形的策略。另一方面,有时序列可能太长,模型无法处理。在这种情况下,你需要将序列截断为较短的长度。
让我们将所有内容打包到一个名为 IMDbDataset 的 Python 类中。我们还将使用这个自定义数据集来创建相应的数据加载器。
encodings 变量存储了大量关于分词文本的信息。我们可以通过字典推导提取出最相关的信息。字典包含:
-
input_ids:是句子中每个标记对应的索引。
-
标签:类别标签
-
attention_mask:指示一个标记是否应该被关注(例如填充标记将不会被关注)。
让我们构建数据集及相应的数据加载器。
加载和微调 BERT
最后,我们完成了数据预处理,可以开始微调我们的模型。我们来定义一个模型和一个优化算法,这里使用的是 Adam。
DistilbertForSequenceCLassification 指定了我们希望微调模型的下游任务,这在这里是序列分类。注意,“uncased”意味着模型不区分大小写字母。
在训练模型之前,我们需要定义一些指标来比较模型的改进。在这个简单的例子中,我们可以使用传统的分类准确率。请注意,这个函数相当长,因为我们按批次加载数据集以绕过 RAM 和 GPU 限制。通常,在微调大型数据集时,这些资源总是不够的。
在 compute_accuracy 函数中,我们加载一个给定的批次,然后从输出中获取预测标签。在此过程中,我们通过变量 num_examples 跟踪示例的总数。同样,我们通过 correct_pred 变量跟踪正确预测的数量。在迭代完整个数据加载器后,我们可以通过最后的除法计算准确率。
你还可以注意到如何在 compute_accuracy 函数中使用模型。我们将 input_ids 以及指示标记是否为实际文本标记或填充的 attention_mask 信息传递给模型。模型返回一个 SequenceClassificatierOutput 对象,我们从中获取 logits 并通过 argmax 函数将其转换为类别。
训练(微调)循环
如果你知道如何在 PyTorch 中编写训练循环,你就不会有任何问题理解这个微调循环。像任何神经网络一样,我们给网络输入数据,计算输出,计算损失,并根据这个损失进行参数更新。
每经过几个周期,我们就打印训练进度以获得反馈。
最终思考
在这篇文章中,我们已经学习了如何仅使用 PyTorch 对像 BERT 这样的预训练大语言模型进行微调。实际上,有一种更快、更智能的方法可以通过 Hugging Face 的 Transformers 库来实现。这个库允许我们创建一个用于微调的 Trainer 对象,我们可以在几行代码中指定如周期数等参数。如果你对下一篇文章中的实现感到好奇,跟随我吧!😉
结束
Marcello Politi
微调更好的聊天模型,采用蒸馏身份偏好优化(IPO)
Mistral 7B 与 IPO 对齐
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 12 月 13 日
--
图片由 Rishabh Dharmani 提供,来源于 Unsplash
要成为聊天模型,预训练的大型语言模型(LLMs)会在配对的指令/问题和预期答案的大型数据集上进行微调。虽然这种简单的微调可以产生令人信服的聊天模型,但它们的回答可能仍然不连贯、有偏见、不道德和不安全。这就是为什么我们通常会进行额外的训练步骤,以更好地使 LLM 与人类对齐。
这种对齐可以通过带有人类反馈的强化学习(RLHF)来完成。正如 OpenAI 和 ChatGPT 的成功所示,RLHF 可以产生最先进的聊天模型。然而,RLHF 的运行成本很高。它需要大量由人类标注的数据集和训练多个辅助模型(参考和奖励模型)。
作为 RLHF 的更简单和更便宜的替代方案,直接偏好优化(DPO)最近已成功应用于对齐 LLMs,如 Hugging Face 的Zephyr和 Intel 的神经聊天。
在本文中,基于 Google DeepMind 的研究,我们将看到尽管 RLHF 和 DPO 在对齐 LLMs 方面表现良好,但由于训练中使用的数据集,它们远未达到最终效果。DeepMind 还展示了为什么 DPO 容易过拟合。我将用通俗易懂的语言解释 DeepMind 提出的替代方案,即身份政策优化(IPO)目标,如何比 RLHF 和 DPO 更简单且更好地从训练数据中学习。
在接下来的部分中,我将展示如何使用 IPO,按照与 Hugging Face 训练 Zephyr 模型类似的训练配方。
我还实现了一个演示 Mistral 7B IPO 训练的笔记本。你可以在这里找到:
描述 IPO 的 DeepMind 论文在 arXiv 上:
ΨPO:偏好优化的泛化
RLHF 和 DPO 在类似的数据集上进行训练:这些数据集包括至少两个由人类(或 LLMs)评分的可能答案。答案被配对,以便在一个配对中,一个答案的评分优于另一个。对于其对齐,我们希望 LLM 学习到哪个答案更受人类欢迎。这就是“偏好优化”。RLHF 通过强化学习来学习这一点,而 DPO 通过简单的分类器来学习。
此外,为了避免过拟合,RLHF 和 DPO 训练必须进行正则化。这种正则化是通过 KL 正则化实现的,控制 LLM 在每一步训练中变得更好,同时不偏离原始(也称为参考)未对齐模型太远。
我在这篇文章中更详细地解释了 KL 正则化在 RLHF 中的工作原理:
[## 在您的 GPU 上使用 DeepSpeed Chat 训练 Instruct LLMs - 第 3 步:与人类的强化学习…
DeepSpeed Chat 实际应用的效率
kaitchup.substack.com](https://kaitchup.substack.com/p/train-instruct-llms-on-your-gpu-with-6a5?source=post_page-----99cddc819a48--------------------------------)
如果没有这种正则化,LLM 会找到一种方法来生成回答,从而最小化训练损失,但这些回答可能没有意义,即它会过拟合训练数据。
DeepMind 证明了 RLHF 和 DPO 是更一般学习目标的特殊情况:Ψ偏好优化(ΨPO)。注意:我不知道 DeepMind 如何发音“ΨPO”,但我猜这可能是 psi-P-O。
DeepMind 指出的另一个弱点,但 LLM 从业者也非常熟悉的是,即使有 KL 正则化,DPO 也容易过拟合。例如,Hugging Face 在他们关于训练 Zephyr 的技术报告中发现,DPO 在仅一个训练周期后就能以完美的准确度过拟合训练数据。
DeepMind 论文的大部分内容致力于证明为什么会发生这种过拟合。我认为这是论文中最复杂的部分。让我们试着用简单的英语来总结一下。
RLHF 和 DPO 在带有偏好注释的答案对上进行训练。通常,在为这种训练创建的数据集中,相同的答案或一个接近的答案总是比另一个更好。在这种情况下,我们可以说它是确定性的:相同的输入总会产生相同的输出。这似乎很直观,但实际上,人们的意见不一致:给定一对答案 A 和 B,一个人可能认为 A 比 B 好,而另一个人可能认为 B 比 A 好。
如果我们想比较两个表现相近的 LLM,即生成的答案仅略有不同,这种情况会经常发生。RLHF 和 DPO 设计用来从这种非确定性数据中学习。
RLHF 和 DPO 为评分的答案分配 Elo 分数,换句话说,它们将成对偏好转换为逐点估计。它们不会直接预测哪个答案更好,而是预测每个答案的(复杂的)分数,以logit偏好的形式。
我们可以说这使得学习目标不必要地复杂化。实际上,当训练数据是确定性的,并且每个提示只使用非常少量的回答时,我们不需要逐点估计。
这就是为什么 DeepMind 提出了另一个更简单的学习目标:身份偏好优化。
身份偏好优化(IPO)
我们看到 DPO 容易出现过拟合。然而,DPO 的优势在于它是一个简单的学习目标,因为它不需要奖励模型,也不进行强化学习。
通过身份偏好优化(IPO)学习目标,DeepMind 提出了一种基于 DPO 的替代方案,它不需要奖励模型,同时更好地利用 KL-正则化。
像 DPO 一样,IPO 是ΨPO 目标的一个特例。IPO 和 DPO 的主要区别在于 IPO 直接学习成对的偏好,而不是 logit-偏好,即 IPO 不使用 Elo 分数。
论文中提供了 IPO 的完整数学证明和公式。他们还比较了 DPO 和 IPO 在 KL-正则化下不同系数的表现(见论文的图 1)。
他们观察到 DPO 对 KL-正则化系数的增加几乎不敏感。另一方面,IPO 受系数增加的影响要大得多。有趣的是,在图 1 的 IPO 曲线中,我们可以看到 IPO 仍能区分输出 y1、y2 和 y3。它将它们排名为 y1 > y2 > y3,而 DPO 认为 y1 是完美的(接近 1.0),而 y2 和 y3 则被认为是同样糟糕的。DPO 出现过拟合,IPO 则没有。
为 Mistral 7B 蒸馏 IPO
IPO 已经在 Hugging Face 的 TRL 中提供,用于对齐 LLM。由于 IPO 是基于 DPO 的,他们直接将其添加到了DPOTrainer中。
我用(几乎)相同的方法对 Mistral 7B 进行了对齐,这些方法受到 Zephyr 模型的启发,详细内容可以参见这篇文章:
[## 便宜的 Zephyr 7B Beta:在消费级硬件上提炼的 DPO
在不使用 A100 GPU 的情况下训练类似 Zephyr 的模型的配方
主要的区别在于使用 IPO 而非 DPO。训练数据由其他 LLM 生成并评估,保持不变。这是一个提炼过的 IPO。
我还对 LoRA 超参数做了小的修改,使用了Hugging Face 对齐手册中提出的参数:
peft_config = LoraConfig(
lora_alpha=64,
lora_dropout=0.1,
r=16,
bias="none",
task_type="CAUSAL_LM",
target_modules= ['k_proj', 'q_proj', 'v_proj', 'o_proj']
)
然后,DPOTrainer 可以简单地配置为使用 IPO,如下所示:
trainer = DPOTrainer(
model,
model_ref,
args=training_arguments,
beta=0.5,
peft_config=peft_config,
train_dataset=dataset_train_dpo,
eval_dataset=dataset_test_dpo,
tokenizer=tokenizer,
loss_type='ipo'
)
“loss_type”被简单地设置为‘ipo’。 “beta”超参数是正则化的系数(tau)。
还值得注意的是,使用 DPO 时,Hugging Face 固定了一个非常低的学习率 5e-7。DPO 很容易过拟合或者在较高学习率下无法收敛。另一方面,使用 IPO 时,我观察到较高的学习率是可行的,但它应该与 beta(正则化的系数)一起调整,以防止过拟合。
结论
正如 DeepMind 所展示的那样,IPO 在理论上比 RLHF 和 DPO 更好,特别是对于用于对齐 LLM 的训练数据集。IPO 直接学习成对的偏好,并更好地利用 KL 正则化以避免过拟合。
由于 IPO 保留了 DPO 的主要优点,同时修正了其缺陷,它应该成为 LLM 对齐的新标准学习目标。
在 Amazon SageMaker 上微调 MPT-7B
原文:
towardsdatascience.com/fine-tune-mpt-7b-on-amazon-sagemaker-1e68e71051fa
照片由 Jeffery Ho 提供,来源于 Unsplash。
学习如何准备数据集并创建一个训练任务,以便在 Amazon SageMaker 上微调 MPT-7B。
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 20 日
--
每周都会有新的大型语言模型(LLMs)被宣布,每个模型都试图超越其前任并占据评估排行榜的首位。其中最新的模型之一是 MPT-7B,由 MosaicML 发布。与同类其他模型不同,这款 70 亿参数的模型是开源的,并且获得了商业使用的许可证 (Apache 2.0 许可证) 🚀。
像 MPT-7B 这样的基础模型是在具有万亿个标记(100 个标记约等于 75 个单词)的数据集上进行预训练的,并且当提示得当时,它们能够生成令人印象深刻的输出。然而,要真正释放大型语言模型在实际应用中的价值,仅仅智能的提示工程可能不足以使其适用于你的用例,因此,需要在领域特定的数据集上对基础模型进行微调。
大型语言模型(LLMs)具有数十亿个参数,因此微调如此庞大的模型是具有挑战性的。好消息是,相比于预训练基础模型,微调的成本更低、速度更快,因为 1) 领域特定的数据集是“较小”的,2) 微调只需对训练数据进行少量遍历。
在本文中我们将学习:
-
如何创建和构建用于微调大型语言模型的数据集。
-
什么是完全分片数据并行的分布式训练作业以及如何配置它。
-
如何定义一个 😊 HuggingFace 估算器。
-
如何在 Amazon SageMaker 中启动一个微调 MPT-7B 的训练作业。
1. 安装依赖项并设置 S3 路径
让我们首先安装 SageMaker Python SDK 和其他一些包。这个 SDK 使得在 AWS 上训练和部署机器学习模型变得可能,只需几行 Python 代码。下面的代码可以在 Github 的 [sagemaker_finetuning.ipynb](https://github.com/jpcpereira/sagemaker-fine-tune-mpt-7b/blob/main/sagemaker_finetuning.ipynb)
notebook 中找到。在 SageMaker Studio、SageMaker notebook 实例或在笔记本电脑上运行 notebook,前提是 认证到 AWS 账户。
!pip install "sagemaker==2.162.0" s3path boto3 --quiet
from sagemaker.huggingface import HuggingFace
from sagemaker.inputs import TrainingInput
from sagemaker import s3_utils
import sagemaker
import boto3
import json
下一步是定义数据将保存到 S3 的路径,并创建一个 SageMaker 会话。
# Define S3 paths
bucket = "<YOUR-S3-BUCKET>"
training_data_path = f"s3://{bucket}/toy_data/train/data.jsonl"
test_data_path = f"s3://{bucket}/toy_data/test/data.jsonl"
output_path = f"s3://{bucket}/outputs"
code_location = f"s3://{bucket}/code"
# Create SageMaker session
sagemaker_session = sagemaker.Session()
region = sagemaker_session.boto_region_name
role = sagemaker.get_execution_role()
2. 构建微调数据集
我们将创建一个虚拟数据集来演示如何微调 MPT-7B。由于在完整数据集上训练此大小的模型需要较长时间且成本较高,因此首先在小数据集上测试和调试训练任务是一个好主意,其次将训练规模扩大到完整数据集。
- 将数据集格式化为字典列表 — 数据集应格式化为字典列表,每个示例具有键值结构,例如,
{
"prompt": "What is a Pastel de Nata?",
"response": "A Pastel de Nata is a Portuguese egg custard tart pastry, optionally dusted with cinnamon."
}
prompt
是给模型的输入(例如,一个问题)。response
是模型训练预测的输出(例如,prompt
中问题的答案)。原始提示通常会经过预处理,以适应提示模板,这有助于模型生成更好的输出。请注意,模型是为因果语言建模训练的,因此可以把它看作是一个“文档完成器”。设计提示模板时,最好让模型觉得它正在完成一个文档。Andrej Karpathy 在他的演讲 State of GPT 中很好地解释了这一机制。
prompt_template = """Write a response that appropriately answers the question below.
### Question:
{question}
### Response:
"""
dataset = [
{"prompt": "What is a Pastel de Nata?",
"response": "A Pastel de Nata is a Portuguese egg custard tart pastry, optionally dusted with cinnamon."},
{"prompt": "Which museums are famous in Amsterdam?",
"response": "Amsterdam is home to various world-famous museums, and no trip to the city is complete without stopping by the Rijksmuseum, Van Gogh Museum, or Stedelijk Museum."},
{"prompt": "Where is the European Parliament?",
"response": "Strasbourg is the official seat of the European Parliament."},
{"prompt": "How is the weather in The Netherlands?",
"response": "The Netherlands is a country that boasts a typical maritime climate with mild summers and cold winters."},
{"prompt": "What are Poffertjes?",
"response": "Poffertjes are a traditional Dutch batter treat. Resembling small, fluffy pancakes, they are made with yeast and buckwheat flour."},
]
# Format prompt based on template
for example in dataset:
example["prompt"] = prompt_template.format(question=example["prompt"])
training_data, test_data = dataset[0:4], dataset[4:]
print(f"Size of training data: {len(training_data)}\nSize of test data: {len(test_data)}")
- 将训练和测试数据上传到 S3 — 一旦训练和测试集准备好并格式化为字典列表,我们将使用下面的工具函数将它们作为 JSON 行上传到 S3:
def write_jsonlines_to_s3(data, s3_path):
"""Writes list of dictionaries as a JSON lines file to S3"""
json_string = ""
for d in data:
json_string += json.dumps(d) + "\n"
s3_client = boto3.client("s3")
bucket, key = s3_utils.parse_s3_url(s3_path)
s3_client.put_object(
Body = json_string,
Bucket = bucket,
Key = key,
)
write_jsonlines_to_s3(training_data, training_data_path)
write_jsonlines_to_s3(test_data, test_data_path)
3. SageMaker 训练任务
使用 S3 中可用的数据集,我们现在将在 Amazon SageMaker 中创建一个训练任务。为此,我们需要创建一个入口点脚本,修改配置文件以指定训练设置,并定义一个 HuggingFace 估算器。我们将(重新)使用来自 LLM Foundry 和 Composer 库的 CLI 启动器,这些启动器设置了分布式训练环境。这两个包均由 MosaicML 维护,该公司是 MPT-7B 的背后公司。工作文件夹应按如下结构组织:
└── **fine-tune-mpt-7b-sagemaker**/
├── training_script_launcher.sh
├── fine_tuning_config.yaml
├── sagemaker_finetuning.ipynb
我们现在将深入了解这些文件中的每一个。
- 创建一个配置文件
[**finetuning_config.yaml**](https://github.com/jpcpereira/sagemaker-fine-tune-mpt-7b/blob/main/finetuning_config.yaml)
— LLM Foundry 仓库中提供的模板是一个很好的起点,特别是[mpt-7b-dolly-sft.yaml](https://github.com/mosaicml/llm-foundry/blob/main/scripts/train/yamls/finetune/mpt-7b_dolly_sft.yaml)
文件。然而,根据数据集大小和训练实例,您可能需要调整一些配置,例如批量大小。我已经修改了该文件以在 SageMaker 中微调模型(请查看[finetuning_config.yaml](https://github.com/jpcpereira/sagemaker-fine-tune-mpt-7b/blob/main/finetuning_config.yaml)
)。您需要关注的参数如下:
max_seq_len: 512
global_seed: 17
...
# Dataloaders
train_loader:
name: finetuning
dataset:
hf_name: json
hf_kwargs:
data_dir: /opt/ml/input/data/train/
...
eval_loader:
name: finetuning
dataset:
hf_name: json
hf_kwargs:
data_dir: /opt/ml/input/data/test/
...
max_duration: 3ep
eval_interval: 1ep
...
global_train_batch_size: 128
...
# FSDP
fsdp_config:
sharding_strategy: FULL_SHARD
mixed_precision: PURE
activation_checkpointing: true
activation_checkpointing_reentrant: false
activation_cpu_offload: false
limit_all_gathers: true
verbose: false
# Checkpoint to local filesystem or remote object store
save_folder: /tmp/checkpoints
dist_timeout: 2000
max_seq_length
指示输入的最大令牌数(记住 100 个令牌约等于 75 个单词)。训练和测试数据将使用 😊 Datasets 库从容器内与训练任务关联的 /opt/ml/input/data/{train, test}
目录加载。查看 SageMaker Training Storage Folders 文档,以了解容器目录的结构。max_duration
指定微调的轮数。两到三轮通常是一个不错的选择。eval_interval
指示模型在测试集上的评估频率。
分布式训练策略是 Fully Sharded Data Parallel (FSDP),这使得像 MPT-7B 这样的巨大模型的训练变得高效。与传统的数据并行策略不同,后者在每个 GPU 中保留模型副本,FSDP 将模型参数、优化器状态和梯度在数据并行工作者之间进行分片。如果您想深入了解 FSDP,可以查看这篇有见地的 PyTorch 介绍文章。FSDP 已集成在 Composer 中,这是 LLM Foundry 使用的分布式训练库。
save_folder
确定模型检查点(.pt
文件)保存的位置。我们将其设置为临时文件夹 /tmp/checkpoints
。
- 创建入口脚本
[**launcher.sh**](https://github.com/jpcpereira/sagemaker-fine-tune-mpt-7b/blob/main/launcher.sh)
— 一个 bash 脚本作为入口点。这个 bash 脚本克隆了 LLM Foundry 仓库,安装了所需的依赖,并且更重要的是,使用 Composer 库的分布式启动器运行训练脚本。请注意,通常在 SageMaker 中,训练作业是通过像python train.py
这样的命令运行训练脚本。然而,在我们的场景中,可以将 bash 脚本作为入口点,这提供了更多的灵活性。最后,我们将保存到/tmp/checkpoints
的模型检查点转换为 HuggingFace 模型格式,并将最终的工件保存到/opt/ml/model/
。SageMaker 会压缩此目录中的所有文件,创建一个 tarballmodel.tar.gz
,并将其上传到 S3。这个 tarball 对于推理非常有用。
# Clone llm-foundry package from MosaicML
# This is where the training script is hosted
git clone https://github.com/mosaicml/llm-foundry.git
cd llm-foundry
# Install required packages
pip install -e ".[gpu]"
pip install git+https://github.com/mosaicml/composer.git@dev
# Run training script with fine-tuning configuration
composer scripts/train/train.py /opt/ml/code/finetuning_config.yaml
# Convert Composer checkpoint to HuggingFace model format
python scripts/inference/convert_composer_to_hf.py \
--composer_path /tmp/checkpoints/latest-rank0.pt \
--hf_output_path /opt/ml/model/hf_fine_tuned_model \
--output_precision bf16
# Print content of the model artifact directory
ls /opt/ml/model/
- 定义 😊 HuggingFace 估算器 — 估算器设置了用于运行训练作业的 Docker 容器。我们将使用一个带有 PyTorch 2.0.0 和 Python 3.10 的镜像。bash 脚本和配置文件会自动上传到 S3,并在容器内可用(由 SageMaker Python SDK 处理)。我们将训练实例设置为
[g5.48xlarge](https://aws.amazon.com/ec2/instance-types/g5/)
,它配备了 8x NVIDIA A10G GPU。[p4d.24xlarge](https://aws.amazon.com/ec2/instance-types/p4/)
也是一个不错的选择。虽然它更贵,但配备了 8x NVIDIA A100 GPU。我们还指明了要在训练集和测试集中跟踪的指标(交叉熵和困惑度)。这些指标的值通过正则表达式捕获并发送到 Amazon CloudWatch。
# Define container image for the training job
training_image_uri = f"763104351884.dkr.ecr.{region}.amazonaws.com/huggingface-pytorch-training:2.0.0-transformers4.28.1-gpu-py310-cu118-ubuntu20.04-v1.1"
# Define metrics to send to CloudWatch
metrics = [
# On training set
{"Name": "train:LanguageCrossEntropy",
"Regex": "Train metrics\/train\/LanguageCrossEntropy: ([+-]?((\d+\.?\d*)|(\.\d+)))"},
{"Name": "train:LanguagePerplexity",
"Regex": "Train metrics\/train\/LanguagePerplexity: ([+-]?((\d+\.?\d*)|(\.\d+)))"},
# On test set
{"Name": "test:LanguageCrossEntropy",
"Regex": "Eval metrics\/eval\/LanguageCrossEntropy: ([+-]?((\d+\.?\d*)|(\.\d+)))"},
{"Name": "test:LanguagePerplexity",
"Regex": "Eval metrics\/eval\/LanguagePerplexity: ([+-]?((\d+\.?\d*)|(\.\d+)))"},
]
estimator_args = {
"image_uri": training_image_uri, # Training container image
"entry_point": "launcher.sh", # Launcher bash script
"source_dir": ".", # Directory with launcher script and configuration file
"instance_type": "ml.g5.48xlarge", # Instance type
"instance_count": 1, # Number of training instances
"base_job_name": "fine-tune-mpt-7b", # Prefix of the training job name
"role": role, # IAM role
"volume_size": 300, # Size of the EBS volume attached to the instance (GB)
"py_version": "py310", # Python version
"metric_definitions": metrics, # Metrics to track
"output_path": output_path, # S3 location where the model artifact will be uploaded
"code_location": code_location, # S3 location where the source code will be saved
"disable_profiler": True, # Do not create profiler instance
"keep_alive_period_in_seconds": 240, # Enable Warm Pools while experimenting
}
huggingface_estimator = HuggingFace(**estimator_args)
⚠️ 确保请求 SageMaker 训练的相应配额,并在使用此酷炫功能时请求 热池 的配额。如果你计划在 SageMaker 中运行多个作业,可以查看 SageMaker 节省计划。
- 启动训练作业 🚀 — 我们已准备好在 Amazon SageMaker 上开始训练作业:
huggingface_estimator.fit({
"train": TrainingInput(
s3_data=training_data_path,
content_type="application/jsonlines"),
"test": TrainingInput(
s3_data=test_data_path,
content_type="application/jsonlines"),
}, wait=True)
训练时间将取决于数据集的大小。使用我们的虚拟数据集,训练大约需要 20min 完成。一旦模型训练完成并转换为 😊 HuggingFace 格式,SageMaker 将把模型 tarball (model.tar.gz
) 上传到 S3 output_path
。我发现实际上上传步骤花费的时间较长(>1h),这可能是由于模型工件压缩的大小(~25GB)。
4. 总结
在本文中,我展示了如何准备数据集并在 SageMaker 中创建训练任务,以微调 MPT-7B 以适应你的使用案例。该实现利用了来自 LLM Foundry 的训练脚本,并使用了 Composer 库的分布式训练启动器。一旦你微调了你的模型并想要部署它,我建议查看 Philipp Schmid 的博客文章;那里有很多关于如何在 SageMaker 中部署 LLM 的示例。祝你玩得愉快,享受你的微调 MPT-7B 模型!🎉
本文中使用的所有代码都可以在 Github 上找到:
## GitHub - jpcpereira/sagemaker-fine-tune-mpt-7b
— 乔昂·佩雷拉
感谢阅读。希望本文能帮助你入门在 Amazon SageMaker 中微调大型语言模型如 MPT-7B。如果你想阅读我未来的文章,请 关注我。非常感谢反馈!如果你有任何问题,请在下面留言,或直接通过 电子邮件 或在 LinkedIn联系我。
优化您的 LLM 而不会让 GPU 负荷过重
原文:
towardsdatascience.com/fine-tune-your-llm-without-maxing-out-your-gpu-db2278603d78
如何在有限的硬件和紧张的预算下优化您的 LLM
·发表于数据科学前沿 ·阅读时间 8 分钟·2023 年 8 月 1 日
--
作者提供的图片:由 Midjourney 生成
定制 LLM 的需求
随着 ChatGPT 的成功,我们见证了对定制大型语言模型需求的激增。
然而,采用的障碍依然存在。由于这些模型体积庞大,对于预算有限的企业、研究人员或爱好者来说,定制这些模型以适应自己的数据集是一项挑战。
现在,通过参数高效优化(PEFT)方法的创新,我们完全有可能以相对较低的成本优化大型语言模型。在本文中,我展示了如何在Google Colab中实现这一目标。
我预计这篇文章对从业者、爱好者、学习者甚至实践型初创企业创始人都将大有裨益。
所以,如果你需要制作一个便宜的原型、测试一个想法,或者创建一个引人注目的数据科学项目——请继续阅读。
为什么我们要优化?
企业通常拥有驱动其某些流程的私有数据集。
举个例子,我曾在一家银行工作,我们将客户投诉记录在 Excel 电子表格中。一名分析师负责(手动)对这些投诉进行分类以便于报告。每月处理成千上万的投诉,这一过程耗时且容易出错。
如果我们拥有足够的资源,我们可以优化一个大型语言模型来进行这一分类,从而通过自动化节省时间,并可能减少错误分类的发生率。
受到这个例子的启发,本文的其余部分展示了如何优化 LLM 以对金融产品和服务的消费者投诉进行分类。
数据集
数据集包含了金融服务和产品的真实消费者投诉数据。这是由消费者金融保护局发布的公开、公开可用的数据。
数据集中有超过 12 万条匿名投诉,分类为大约 214 个“子问题”。
我在我的 Hugging Face 页面上有一个版本的数据集,你可以自己探索。
硬件
我用于训练的硬件是通过 Google Colab 访问的 16GB RAM 的 V100 GPU。这是一种相对便宜且可访问的基础设施,通过 Google Colab Pro 以约 9.99 美元每 100 计算单元租用。
大型语言模型
使用的 LLM 是 RoBERTa¹(XLM),其参数约为 5.63 亿。模型及其规格的概述可以在这里找到。
虽然目前不是最大的模型,但 RoBERTa 对于仅能访问小规模基础设施的用户来说仍然是一个挑战。这使其成为展示在小规模基础设施上训练相对较大模型可行性的理想选择。
注意 — RoBERTa 是从 Hugging Face Hub中提取的预训练模型。
使用 LoRA 低成本微调
如引言中所述,PEFT 方法使得低成本微调 LLMs 成为可能。其中一种方法是 LoRA,即大型语言模型的低秩适配。
从高层次来看,LoRA 实现了两件事。首先,它冻结了 LLM 的现有权重(使其不可训练);其次,它将可训练的“低维”层注入到架构的指定层中。
这种技术产生了一个可训练参数远少于标准微调的模型,同时保持性能。与标准微调相比,LoRA 已被证明能将 GPU 内存消耗降低三倍。
关于 LoRA 的更多详细信息,请阅读完整的论文。
技术细节
过去,在有限硬件上训练大型语言模型的主要挑战是调整训练参数,以防止因超出 GPU 内存容量而导致过程崩溃。
使用 LoRA,可以通过一些调整突破硬件的限制。
应用 LoRA
假设你已经准备好数据集,首先需要做的是设置 LoRA 配置。
作者脚本:将 LoRA 应用于 RoBERTa
-
task_type — 你微调模型的任务。对于投诉分类,我们专注于序列分类。
-
r — 一个名为 LoRA 注意力维度的超参数,影响缩放。
-
lora_alpha — 另一个影响缩放的超参数。
-
target_modules — 在这里我们指定变换应应用于我们变换器中的注意力模块,因此我们将其设置为“query”和“value”。
-
lora_dropout — LoRA 层的丢弃概率。
-
bias — LoRA 的偏置类型。
-
modules_to_save — 除了 LoRA 层,我们声明希望使模型的哪些层可训练并在最终检查点保存。对于我们的目的,我们需要将分类头设置为可训练。
脚本的下一部分设置了模型本身。这里我们只是从 Hugging Face 加载预训练的 RoBERTa 模型。额外的模型参数仅仅是将字典传递给分类 ID,使得返回的标签为文本形式而非数字编码。
最后,我们希望基于 LoRA 配置转换预训练模型。这是通过 get_peft_model 函数完成的。
应用 LoRA 后,我们得到一个具有以下结构的网络:
trainable params: 4,110,764
all params: 562,951,596
trainable%: 0.7302162440267778
注意:原始的 RoBERTa 参数仍然存在于网络中;它们只是被冻结,因此不可训练。相反,我们有 LoRA 层,它们数量较少且可训练。
启动训练
一旦我们有了 LoRA 模型,接下来只需设置训练器并启动训练过程。
脚本作者:LoRA 模型训练设置
脚本中的大多数方面在训练深度学习模型时都是标准的;然而,我们加入了一些额外的元素来提高 GPU 内存效率。让我们简要概述一下它们。
-
Per_device_train_batch_size — 将此参数设置为较低的值可以保持 GPU 的低 RAM 使用,但会降低训练速度。数值越低,完成一个 epoch 所需的训练步骤就越多。请记住,一个 epoch 是对整个训练数据集的完整运行。
-
Per_device_eval_batch — 设置此参数决定了每个 GPU 核心处理多少数据样本。增加此数值会提高评估速度,从而提高训练速度,但会增加 GPU 内存使用。
-
Gradient_accumulation_steps — 通常,在更新深度学习模型时,网络会进行前向传播并计算损失。随后,通过称为反向传播的过程,计算梯度并用以更新模型参数。使用梯度累积时,模型参数不会在一次前向传播后立即更新。相反,梯度会在指定的批次数中存储和累积。只有在所有批次通过网络后,累积的梯度才会应用于更新网络中的参数。其效果是增加有效批量大小,即模型可以有效地在更大的批量大小上进行训练,从而在 GPU 内存使用不增加的情况下,享受训练更大批量的好处。
-
Fp16(混合精度训练) — 将混合精度训练设置为 true 可以帮助提高训练速度,但代价是 GPU 使用量增加。本质上,网络中的一些计算以 16 位(半精度)而非 32 位(全精度)进行,以加快计算速度。然而,这种方法需要在 GPU 设备上存储 16 位和 32 位版本的模型,即 1.5 倍的 RAM 使用量。
模型诊断
让我们看看一切的表现。由于这涉及到在有限硬件下有效训练,让我们从 GPU RAM 诊断开始。
图片作者提供:GPU RAM 使用图表
我们能够在不超出 GPU RAM 的情况下训练模型 9 小时。这是一个不错的结果。
那么模型的表现如何?让我们看看验证和训练损失。
图片作者提供:验证和训练损失
通过检查损失图表,似乎模型尚未收敛,尽管训练过程持续了大约九小时。
考虑到模型的庞大尺寸和我们有限的基础设施,以及许多训练参数决策优先考虑内存保存而非训练速度,这可能并不令人惊讶。
模型的精准度、召回率和 F1 指标显示,它可能还没有准备好进行全面的生产使用。
图片作者提供:模型的精准度和召回率跟踪
图片作者提供:模型的 F1 跟踪
成本
Google 根据计算单元对通过 Colab 租用 GPU 收取费用。因此,要计算训练模型九小时的成本,我们需要确定使用的计算单元数量。我无法从 Google 获得有关这些单元计算的明确细节,但有建议计算单元的定义如下:
1 GB 内存 x 1 小时 = 1 个计算单元
模型训练了大约九小时,并使用了约 15 GB 的 RAM。因此,训练过程消耗了大约 135 个计算单元。以每 100 个计算单元 9.99 美元的价格,训练模型的成本大约为 13.49 美元。
为了使模型达到可用状态,收敛可能需要稍微高一点的开支。
结论
我们已经证明 LoRA 使得资源有限的个人能够参与 LLM 的世界。对于感兴趣的读者,我推荐探索其他参数高效微调方法,以及改进训练过程中数据效率的策略。
-
带有完整模型运行的端到端 Colab 可以在这里获取。
-
我已托管一个演示该模型的应用程序,Hugging Face供你自行尝试。
-
训练运行的完整模型诊断信息可以在这里查看。
-
模型检查点的链接在这里。
-
在YouTube上观看直播教程。
感谢阅读。
如果你渴望提升在人工智能方面的技能,加入我的课程的等待名单吧,在课程中我将引导你开发大型语言模型驱动的应用程序。
如果你正在寻求为你的业务进行 AI 转型,今天就预约一次发现电话吧。
在 Brainqub3,我们开发定制的 AI 软件。我们使用最新的 AI 技术创建 qub3s,先进的人工智能大脑,以...
欲获取更多关于人工智能、数据科学和大型语言模型的见解,你可以订阅YouTube频道。
引用
[1] Conneau, A., Khandelwal, K., Goyal, N., Chaudhary, V., Wenzek, G., Guzmán, F., Grave, E., Ott, M., Zettlemoyer, L., & Stoyanov, V. (2019). 大规模无监督跨语言表示学习。CoRR。取自arxiv.org/abs/1911.02116
在 Colab 笔记本中微调你自己的 Llama 2 模型
原文:
towardsdatascience.com/fine-tune-your-own-llama-2-model-in-a-colab-notebook-df9823a04a32
实用的 LLM 微调介绍
·发表于 Towards Data Science ·12 分钟阅读·2023 年 7 月 25 日
--
作者提供的图片
随着 LLaMA v1 的发布,我们见证了微调模型的寒武纪大爆发,包括Alpaca、Vicuna和WizardLM等。这一趋势促使不同的公司推出了适合商业使用的基础模型许可证,如OpenLLaMA、Falcon和XGen等。Llama 2 的发布现将两者的最佳元素结合起来:提供了高效的基础模型和更宽松的许可证。
在 2023 年上半年,软件领域受到了API 的广泛使用(如 OpenAI API)的重大影响,利用大型语言模型(LLMs)构建基础设施。LangChain和LlamaIndex等库在这一趋势中发挥了关键作用。进入下半年,微调(或指令调优)这些模型将成为 LLMOps 工作流中的标准流程。这一趋势受到多种因素推动:节省成本的潜力、处理机密数据的能力,甚至是开发超越 ChatGPT 和 GPT-4 等显著模型在特定任务中的表现的模型的潜力。
在本文中,我们将探讨为什么指令调优有效,以及如何在 Google Colab 笔记本中实现它,以创建你自己的 Llama 2 模型。与往常一样,代码可在Colab和GitHub上找到。
🔧 微调 LLMs 的背景
图片由作者提供
LLMs 在大量文本语料库上进行预训练。以Llama 2为例,我们对训练集的组成知之甚少,除了它有 2 万亿个标记。相比之下,BERT(2018 年)“仅”在 BookCorpus(8 亿词)和英语维基百科(25 亿词)上进行过训练。从经验来看,这是一个非常昂贵且漫长的过程,并且有很多硬件问题。如果你想了解更多,我推荐阅读Meta 的日志,了解 OPT-175B 模型的预训练情况。
当预训练完成后,像 Llama 2 这样的自回归模型可以预测序列中的下一个标记。然而,这并没有使它们成为特别有用的助手,因为它们不会对指令作出回应。这就是我们采用指令调优来使它们的回答与人类期望对齐的原因。主要有两种微调技术:
-
监督微调(SFT):模型在一组指令和响应的数据集上进行训练。它调整 LLM 中的权重,以最小化生成的答案与真实响应之间的差异,作为标签。
-
来自人类反馈的强化学习(RLHF):模型通过与环境互动并接收反馈来学习。它们被训练以最大化奖励信号(使用PPO),这个信号通常来源于人类对模型输出的评估。
一般来说,RLHF 被证明能够捕捉到更复杂和细致的人类偏好,但也更难有效实施。确实,它需要精心设计奖励系统,并且对人类反馈的质量和一致性较为敏感。未来的一个可能替代方案是直接偏好优化(DPO)算法,它直接在 SFT 模型上运行偏好学习。
在我们的案例中,我们将执行 SFT,但这提出了一个问题:为什么微调在第一时间会有效?正如Orca 论文中所强调的,我们的理解是微调利用了在预训练过程中获得的知识。换句话说,如果模型从未见过你感兴趣的数据类型,微调将无济于事。然而,如果是这种情况,SFT 可以表现得极其优秀。
例如,LIMA 论文展示了如何通过在仅 1,000 个高质量样本上微调一个具有 65 亿参数的 LLaMA(v1)模型来超越 GPT-3(DaVinci003)。指令数据集的质量至关重要,以达到这种性能水平,这就是为什么许多工作集中在这个问题上(如evol-instruct、Orca 或phi-1)。请注意,LLM 的大小(65b,而不是 13b 或 7b)对有效利用已有知识也是至关重要的。
与数据质量相关的另一个重要点是提示模板。提示由类似的元素组成:用于指导模型的系统提示(可选),用于提供指令的用户提示(必需),需要考虑的额外输入(可选),以及模型的回答(必需)。在 Llama 2 的情况下,作者使用了以下聊天模型的模板:
<s>[INST] <<SYS>>
System prompt
<</SYS>>
User prompt [/INST] Model answer </s>
还有其他模板,比如来自 Alpaca 和 Vicuna 的模板,它们的影响还不太明确。在这个例子中,我们将重新格式化我们的指令数据集以符合 Llama 2 的模板。为了本教程的目的,我已经使用了优秀的[timdettmers/openassistant-guanaco](https://huggingface.co/datasets/timdettmers/openassistant-guanaco)
数据集。你可以在 Hugging Face 上找到它,名称为[mlabonne/guanaco-llama2-1k](https://huggingface.co/datasets/mlabonne/guanaco-llama2-1k)
。请注意,如果你使用的是基础版 Llama 2 模型而不是聊天版本,你不需要遵循特定的提示模板。
🦙 如何微调 Llama 2
在本节中,我们将使用 Google Colab(2.21 信用/小时)在配备高 RAM 的 T4 GPU 上微调一个具有 70 亿参数的 Llama 2 模型。请注意,T4 的 VRAM 仅为 16 GB,这勉强足够存储 Llama 2–7b 的权重(7b × 2 字节 = 14 GB 的 FP16)。此外,我们还需要考虑优化器状态、梯度和前向激活的开销(有关更多信息,请参见这篇优秀的文章)。这意味着在这里进行全面微调是不可能的:我们需要像LoRA或QLoRA这样的参数高效微调(PEFT)技术。
为了大幅减少 VRAM 使用量,我们必须用 4 位精度微调模型,这就是我们在这里使用 QLoRA 的原因。好消息是我们可以利用 Hugging Face 生态系统中的transformers
、accelerate
、peft
、trl
和bitsandbytes
库。我们将在以下代码中进行操作,基于 Younes Belkada 的GitHub Gist。首先,我们安装并加载这些库。
!pip install -q accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
HfArgumentParser,
TrainingArguments,
pipeline,
logging,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
让我们稍微讨论一下可以在这里调整的参数。首先,我们希望加载一个llama-2-7b-chat-hf
模型(chat 模型),并在mlabonne/guanaco-llama2-1k
(1,000 个样本)上训练它,这将生成我们微调后的模型llama-2-7b-miniguanaco
。如果你对如何创建这个数据集感兴趣,可以查看这个笔记本。可以随意更改:在Hugging Face Hub上有很多好的数据集,比如[databricks/databricks-dolly-15k](https://huggingface.co/datasets/databricks/databricks-dolly-15k)
。
QLoRA 将使用 64 的秩和 16 的缩放参数(有关 LoRA 参数的更多信息,请参见这篇文章)。我们将使用 NF4 类型以 4 位精度直接加载 Llama 2 模型,并训练一个周期。有关其他参数的更多信息,请查看TrainingArguments、PeftModel和SFTTrainer 文档。
# The model that you want to train from the Hugging Face hub
model_name = "NousResearch/Llama-2-7b-chat-hf"
# The instruction dataset to use
dataset_name = "mlabonne/guanaco-llama2-1k"
# Fine-tuned model name
new_model = "llama-2-7b-miniguanaco"
################################################################################
# QLoRA parameters
################################################################################
# LoRA attention dimension
lora_r = 64
# Alpha parameter for LoRA scaling
lora_alpha = 16
# Dropout probability for LoRA layers
lora_dropout = 0.1
################################################################################
# bitsandbytes parameters
################################################################################
# Activate 4-bit precision base model loading
use_4bit = True
# Compute dtype for 4-bit base models
bnb_4bit_compute_dtype = "float16"
# Quantization type (fp4 or nf4)
bnb_4bit_quant_type = "nf4"
# Activate nested quantization for 4-bit base models (double quantization)
use_nested_quant = False
################################################################################
# TrainingArguments parameters
################################################################################
# Output directory where the model predictions and checkpoints will be stored
output_dir = "./results"
# Number of training epochs
num_train_epochs = 1
# Enable fp16/bf16 training (set bf16 to True with an A100)
fp16 = False
bf16 = False
# Batch size per GPU for training
per_device_train_batch_size = 4
# Batch size per GPU for evaluation
per_device_eval_batch_size = 4
# Number of update steps to accumulate the gradients for
gradient_accumulation_steps = 1
# Enable gradient checkpointing
gradient_checkpointing = True
# Maximum gradient normal (gradient clipping)
max_grad_norm = 0.3
# Initial learning rate (AdamW optimizer)
learning_rate = 2e-4
# Weight decay to apply to all layers except bias/LayerNorm weights
weight_decay = 0.001
# Optimizer to use
optim = "paged_adamw_32bit"
# Learning rate schedule (constant a bit better than cosine)
lr_scheduler_type = "constant"
# Number of training steps (overrides num_train_epochs)
max_steps = -1
# Ratio of steps for a linear warmup (from 0 to learning rate)
warmup_ratio = 0.03
# Group sequences into batches with same length
# Saves memory and speeds up training considerably
group_by_length = True
# Save checkpoint every X updates steps
save_steps = 25
# Log every X updates steps
logging_steps = 25
################################################################################
# SFT parameters
################################################################################
# Maximum sequence length to use
max_seq_length = None
# Pack multiple short examples in the same input sequence to increase efficiency
packing = False
# Load the entire model on the GPU 0
device_map = {"": 0}
现在我们可以加载所有内容并开始微调过程。我们依赖多个包装器,请耐心等待。
-
首先,我们要加载我们定义的数据集。在这里,我们的数据集已经过预处理,但通常,这时你会重新格式化提示、筛选掉不良文本、合并多个数据集等。
-
然后,我们正在配置
bitsandbytes
以进行 4 位量化。 -
接下来,我们正在使用相应的分词器将 Llama 2 模型以 4 位精度加载到 GPU 上。
-
最后,我们正在加载 QLoRA 配置、常规训练参数,并将所有内容传递给
SFTTrainer
。训练可以最终开始!
# Load dataset (you can process it here)
dataset = load_dataset(dataset_name, split="train")
# Load tokenizer and model with QLoRA configuration
compute_dtype = getattr(torch, bnb_4bit_compute_dtype)
bnb_config = BitsAndBytesConfig(
load_in_4bit=use_4bit,
bnb_4bit_quant_type=bnb_4bit_quant_type,
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=use_nested_quant,
)
# Check GPU compatibility with bfloat16
if compute_dtype == torch.float16 and use_4bit:
major, _ = torch.cuda.get_device_capability()
if major >= 8:
print("=" * 80)
print("Your GPU supports bfloat16: accelerate training with bf16=True")
print("=" * 80)
# Load base model
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map=device_map
)
model.config.use_cache = False
model.config.pretraining_tp = 1
# Load LLaMA tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# Load LoRA configuration
peft_config = LoraConfig(
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
r=lora_r,
bias="none",
task_type="CAUSAL_LM",
)
# Set training parameters
training_arguments = TrainingArguments(
output_dir=output_dir,
num_train_epochs=num_train_epochs,
per_device_train_batch_size=per_device_train_batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
optim=optim,
save_steps=save_steps,
logging_steps=logging_steps,
learning_rate=learning_rate,
weight_decay=weight_decay,
fp16=fp16,
bf16=bf16,
max_grad_norm=max_grad_norm,
max_steps=max_steps,
warmup_ratio=warmup_ratio,
group_by_length=group_by_length,
lr_scheduler_type=lr_scheduler_type,
report_to="tensorboard"
)
# Set supervised fine-tuning parameters
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
dataset_text_field="text",
max_seq_length=max_seq_length,
tokenizer=tokenizer,
args=training_arguments,
packing=packing,
)
# Train model
trainer.train()
# Save trained model
trainer.model.save_pretrained(new_model)
作者提供的图片
训练可能会很长时间,这取决于数据集的大小。在这里,T4 GPU 上花了不到一个小时。我们可以在 tensorboard 上检查图表,如下所示:
%load_ext tensorboard
%tensorboard --logdir results/runs
作者提供的图片
让我们确保模型行为正确。这需要更全面的评估,但我们可以使用文本生成管道来提出诸如“什么是大语言模型?”这样的问题。请注意,我正在格式化输入以匹配 Llama 2 的提示模板。
# Ignore warnings
logging.set_verbosity(logging.CRITICAL)
# Run text generation pipeline with our next model
prompt = "What is a large language model?"
pipe = pipeline(task="text-generation", model=model, tokenizer=tokenizer, max_length=200)
result = pipe(f"<s>[INST] {prompt} [/INST]")
print(result[0]['generated_text'])
模型输出了以下响应:
A large language model is a type of artificial intelligence that is trained on a large dataset of text to generate human-like language. It is typically trained on a dataset of text that is much larger than the dataset used for smaller language models. The large dataset allows the model to learn more complex patterns in language, which can result in more accurate and natural-sounding language generation.Large language models are often used for tasks such as text summarization, language translation, and chatbots. They are also used for more complex tasks such as writing articles, generating creative content, and even composing music.Large language models are trained using a technique called deep learning, which involves using many layers of artificial neural networks to learn complex patterns in the data. The model is trained on a large dataset of text, and the neural networks are trained to predict the next word in a sequence of text given
根据经验,对于只有 70 亿参数的模型来说,它非常一致。你可以尝试使用评估数据集中的更难问题,如BigBench-Hard。Guanaco 是一个优秀的数据集,以前生产过高质量的模型。你可以使用[mlabonne/guanaco-llama2](https://huggingface.co/datasets/mlabonne/guanaco-llama2)
在整个数据集上训练 Llama 2 模型。
现在我们如何存储我们新的 llama-2-7b-miniguanaco
模型?我们需要将 LoRA 的权重与基础模型合并。不幸的是,据我所知,没有简单的方法来做到这一点:我们需要以 FP16 精度重新加载基础模型,并使用 peft
库来合并所有内容。可惜的是,这也会引发 VRAM 的问题(尽管已经清空),所以我建议重启笔记本,重新执行前三个单元格,然后执行下一个单元格。如果你知道解决办法,请联系我!
# Reload model in FP16 and merge it with LoRA weights
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
low_cpu_mem_usage=True,
return_dict=True,
torch_dtype=torch.float16,
device_map=device_map,
)
model = PeftModel.from_pretrained(base_model, new_model)
model = model.merge_and_unload()
# Reload tokenizer to save it
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.add_special_tokens({'pad_token': '[PAD]'})
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
我们的权重已合并并重新加载了分词器。现在我们可以将所有内容推送到 Hugging Face Hub 以保存我们的模型。
!huggingface-cli login
model.push_to_hub(new_model, use_temp_dir=False)
tokenizer.push_to_hub(new_model, use_temp_dir=False)
现在,你可以像加载 Hub 上的其他 Llama 2 模型一样使用这个模型进行推理。也可以重新加载它以进行更多的微调——也许用另一个数据集?
如果你认真考虑微调模型,建议使用脚本而不是笔记本。你可以在 Lambda Labs、Runpod、Vast.ai 上以低于 0.3$/小时的价格轻松租用 GPU。一旦连接,你可以安装库、导入脚本、登录 Hugging Face 和其他工具(如 Weights & Biases 用于记录实验),然后开始微调。
trl
脚本目前非常有限,因此我基于之前的笔记本制作了自己的版本。你可以在GitHub Gist 上找到它。如果你在寻找全面的解决方案,可以查看来自 OpenAccess AI Collective 的 Axolotl,它也原生支持多个数据集、Deepspeed、Flash Attention 等。
结论
在这篇文章中,我们展示了如何使用 Colab 笔记本对 Llama 2 7b 模型进行微调。我们介绍了一些 LLM 训练和微调的必要背景,以及与指令数据集相关的重要考虑因素。在第二部分,我们成功地微调了 Llama 2 模型,使用了其原生提示模板和自定义参数。
这些微调后的模型可以集成到 LangChain 和其他架构中,作为 OpenAI API 的有利替代方案。记住,在这个新范式中,指令数据集是新的黄金,你的模型的质量在很大程度上取决于其微调的数据。因此,祝你构建高质量数据集好运!
如果你对更多关于 LLM 的内容感兴趣,可以在 Twitter 上关注我 @maximelabonne。
参考资料
-
Hugo Touvron, Thomas Scialom 等人(2023 年)。Llama 2: Open Foundation and Fine-Tuned Chat Models。
-
Philipp Schmid, Omar Sanseviero, Pedro Cuenca, & Lewis Tunstall. Llama 2 已上线——在 Hugging Face 上获取它。
huggingface.co/blog/llama2
-
罗汉·塔奥里、伊莎恩·古尔拉贾尼、张天艺、扬·杜布瓦、李雪辰、卡洛斯·盖斯特林、珀西·梁及辰野博。 (2023). 斯坦福阿尔帕卡:一种遵循指令的 LLaMA 模型。
-
雅各布·德夫林、明伟·张、肯顿·李及克里斯蒂娜·托特诺娃。 (2019). BERT:用于语言理解的深度双向变换器的预训练。
-
提姆·德特默斯、阿尔蒂多罗·帕尼奥尼、阿里·霍尔茨曼及卢克·泽特尔摩耶。 (2023). QLoRA:高效微调量化 LLM。
相关文章
使用 AutoGPTQ 对自己的 LLM 进行量化
towardsdatascience.com ## 大型语言模型中的解码策略
从束搜索到核采样的文本生成指南
towardsdatascience.com
了解更多机器学习知识,并通过点击支持我的工作——成为 Medium 会员:
[## 通过我的推荐链接加入 Medium - Maxime Labonne
作为 Medium 会员,你的会员费的一部分会支持你阅读的作者,同时你可以全面访问每一个故事……
medium.com](https://medium.com/@mlabonne/membership?source=post_page-----df9823a04a32--------------------------------)
如果你已经是会员,你可以 在 Medium 上关注我。
微调的 LLMs 用于情感预测——如何分析和评估
在 Hugging Face 上进行情感预测模型的评估
·发表于Towards Data Science ·阅读时间 10 分钟·2023 年 8 月 9 日
--
情感分析是在大语言模型(LLMs)时代经历了显著转变的领域。由于 LLMs 能够理解文本的上下文,它们被证明是分析情感的强大工具。在 Hugging Face 上,提供的情感分析 LLMs 数量令人印象深刻。上次我检查时,在编写本文时,Hugging Face 上用于情感任务的模型数量为3017! 这是一个相当庞大的数字。曾经用传统的机器学习技术,如 TFIDF 特征、计数正面和负面词汇,或使用如 VADER 这样的库进行情感分析的时代已经过去。
尽管可用的模型数量庞大令人兴奋,但也可能让人感到不知所措。因此,本文将帮助你在情感分析的 LLM 丛林中导航。我将挑选出顶级模型,并展示如何分析和评估它们。这将帮助你更好地理解哪个模型适合你的情感分析需求。
为什么你需要用你的数据分析和评估模型
情感分析是一个非常重要的商业 KPI。许多企业根据客户评论的情感分析来做出重要决策,如产品推广或停产。
大多数 Hugging Face 上的微调模型已经提供了分析和评估功能。因此,你可能会问,为什么还需要进行自己的分析和评估。原因有很多:
-
模型开发人员提供的评估基于他们的数据,这可能并不反映您的业务情况。
-
即使所有模型都被称为情感分析模型,并不是所有模型都可能适合您的业务用例。
-
情感分析的战略重要性要求基于您的具体业务数据进行分析和评估。
方法
我将在这个故事中采取的方法如图所示。我将首先选择几个候选模型,然后建立评估标准。所有模型将用于对一个共同数据集进行情感预测。输出将被分析并与评估标准进行比较。
方法(图片由作者提供)
请注意,这里的评估纯粹是从情感分析的角度,而不是从技术性能的角度。
待分析的候选模型
我将采用下表中的模型进行分析。选择这些模型的原因是,它们是撰写此故事时下载量最高的模型,并且它们都使用不同的基础模型。预测的情感类型也不同。分析这些模型将帮助我们全面了解如何使用微调的 LLM 来预测情感。
候选模型(图片由作者提供)
您可能会观察到这些模型既有通用文本分析也有推文分析的混合。
您可以将这里描述的方法应用于各种您感兴趣的模型。
用于分析模型输出的数据
为了分析这些模型,让我们从亚马逊食品评论中获取一个数据集。下面显示了一个示例数据。
客户评论示例(图片由作者提供)
我选择客户评论数据集的原因是,它包含真实的客户评论。与推文等替代数据集相比,它还包含较长且复杂的评论。在企业中,分析客户评论比分析推文更为重要。此外,推文的字符数是有限制的,而客户评论可以有非常长的文本。
评估方法 — 与真实数据比较
除了分析这些模型之外,评估它们并通过与真实数据比较结果也是很有用的。实际的评审数据有一个由客户给出的 1 到 5 的评分。可以通过以下视觉表示来展示真实数据,这是一张基于客户实际评分的直方图。
真实数据(图片由作者提供)
实际数据有一个 1 到 5 的评分,但没有负面、中性或正面的情感。然而,我们可以假设评分为 1 和 2 为负面情感,评分为 3 为中性情感,评分为 4 和 5 为正面情感。
所以我们的真实数据表将如下所示。
实际数据(图片由作者提供)
现在让我们看看各种模型在实际数据上的表现。这里是使用各种模型进行的情感分析。
微调的 LLM 对情感的预测为评分
nlptown/bert-base-multilingual-uncased-sentiment是下载量最多的模型之一。它是基于 BERT 基础(双向编码器表示的变换器)的模型。它经过微调以预测评论的情感为星级(1 到 5)。它适用于未大小写的文本,并且期望最大令牌长度为 512。它可以用六种语言 — 英语、荷兰语、德语、法语、意大利语和西班牙语。
这里展示了情感分析。
bert-base-multilingual-uncased-sentiment 分析(图片由作者提供)
由于数据是客户评论数据,使用星级评分的方法进行情感分析是很好的。我们可以将其与实际客户评分进行比较,如下所示。你可以观察到预测的评分与实际评分一致。模型在预测情感方面表现良好。
bert-base-multilingual-uncased-sentiment 的预测与实际评分(图片由作者提供)
与实际数据比较,得到的平均绝对误差百分比为 8.43%
nlptown/bert-base-multilingual-uncased-sentiment 评估(图片由作者提供)
现在让我们进入下一个微调的大型语言模型。
微调的 LLM 对情感的预测为负面、中性、正面
在这里,我们将分析cardiffnlp/twitter-roberta-base-sentiment-latest模型。基础模型 RoBERTA(一种稳健优化的 BERT 预训练方法)是 BERT 的一个变体,由 Facebook 引入。cardiffnlp/twitter-roberta-base-sentiment-latest 模型经过社交媒体推文的微调,以预测情感为负面、中性或正面。情感预测结果在这里显示。
cardiffnlp/twitter-roberta-base-sentiment 分析(图片由作者提供)
与实际数据比较,得到的平均绝对误差百分比为 1.86%
cardiffnlp/twitter-roberta-base-sentiment 评估(图片由作者提供)
结果非常出色,尤其是考虑到模型是在推文上微调的,而不是在实际客户评论上。
现在让我们进入下一个模型。
微调的 LLM 对情感的预测为一种情感
情感可以以各种形式表达,其中一种方式是情感。 bhadresh-savani/distilbert-base-uncased-emotion模型预测六种情感——悲伤、喜悦、爱、愤怒、恐惧和惊讶。它经过精细调优,基于 distilbert,这是 BERT 的一个更小、更快的变体。它是由研究人员设计的,具有更小的内存占用。情感分析结果如下所示。
bhadresh-savani/distilbert-base-uncased-emotion 分析(图片来源于作者)
这个分析很有趣,因为情感是分析客户评价的一个很好的方式。然而,将情感与情绪联系起来并不是直截了当的。这是一张视觉图,可以帮助你理解情感与情绪之间的联系。
情感与情绪(图片来源于作者)
你会发现,在 5 星实际评分中包含了所有类型的情感:喜悦、愤怒、恐惧、爱、悲伤和惊讶。这里有一个例子,情感是恐惧,但评分却是 5 星。
恐惧情感,但评分是 5 星(图片来源于作者)
评论显示客户对改变狗的食物感到紧张和恐惧。然而,购买的产品很好,帮助克服了恐惧,并解决了问题。
分析情感也有助于理解客户购买的原因。然而,这与实际情况进行比较是困难的。
因此,最终的评估表如下所示:
所有评估(图片来源于作者)
你可以将这里描述的方法应用于各种其他经过精细调优的 LLMs 进行情感分析。
结论
情感分析对许多企业非常重要,因为它有助于更好地理解客户和产品的表现。经过精细调优的 LLMs 提供了分析 LLMs 的前沿方法。由于精细调优的模型数量不断增加,了解各种类型的模型及其评估方法是有用的。
在这个故事中,你看到了三种情感预测类型——评分预测、负面/中性/正面情感预测和情感预测。它们都可以用于各种业务场景,以更好地理解客户情感。你看到了如何分析它们以及评估结果。
由于有很多经过精细调优的大型语言模型,你可能不希望随意尝试模型。因此,这里提供了一些关于如何选择可以实验的模型的指南:
业务用例需求: 理解你对情感分析的需求是将你引向正确方向的主要起点。不同的行业可能对业务用例中的情感分析有不同的要求。例如,银行业的用例可能需要将情感分析分为积极、中立或消极。然而,电子商务零售商可能需要基于 1 到 5 的评级尺度进行情感分析。理解需求可以帮助你准确确定需要实验的模型范围。
数据类型: 需要情感分析的数据类型多种多样。例如,客户评价和推文是显而易见的,但还有许多不同类型的数据需要进行情感分析,如电子邮件、质量检查记录、用户提交的内容、调查反馈、员工反馈、医疗反馈等。重要的是要分析大语言模型的微调数据类型。一般来说,选择一个与数据类型相同的微调模型更为合适。对与微调数据类型不同的数据进行情感分析可以作为例外。例如,你可以使用一个在推文上微调的模型来对客户评价进行情感分析。然而,使用一个在推文上微调的模型来对员工反馈进行情感分析可能效果不佳。因此,尽量选择一个在与你的数据相关的数据上进行微调的模型。
基础模型和训练参数: 你应该仔细观察基础模型以及使用的训练参数。这可以决定你可能需要应用的限制条件,如令牌的最大长度或文本的大小。例如,如果基础模型的最大长度为 512,而你的文本包含更多的令牌,那么微调模型可能无法给出正确的结果。因此,尝试理解基础模型和训练参数,以确定你可能需要应用的所有限制条件。分析这些限制可以帮助你判断模型是否适合你的需求。
多语言能力: 随着生成式人工智能对企业战略重要性的增加,大多数项目都具有全球性质。这意味着情感分析项目本质上是全球性的,应该考虑不同的语言。这意味着你选择的模型应该具有多语言能力,或者你应该根据不同的语言选择多个模型,以满足情感分析的需求。
评估标准:尽管已经有微调模型可用且可以直接使用,但你仍然应该设定一些评估标准。评估可以依赖于你可能拥有的某些真实信息,也可以是人工评估。评估标准需要是客观的,例如模型输出与真实值之间的平均绝对误差,就像我在这个故事中使用的那样。你会发现,如果真实值是评分,情感分析模型对评估并不有用。设定评估标准还可以帮助你决定要实验哪些模型。
技术实现
这里有一个 Python 代码片段,使用经过微调的模型来预测情感。
import pandas as pd
from transformers import pipeline
##Read data
file_name = 'path_to_file'
df = pd.read_csv(file_name)
col_txt = "reviews"
##Get hugging face model
task = "sentiment-analysis"
model = "nlptown/bert-base-multilingual-uncased-sentiment"
sentiment_model = pipeline(task,model=model)
##Get Sentiment
lst_txt = list(df[col_txt])
lst_sentiment = sentiment_model(lst_txt)
df['sentiment'] = [s['label'] for s in lst_sentiment]
df['sentiment_score'] = [s['score'] for s in lst_sentiment]
数据集引用
数据集可在这里获得,许可证为 CC0 公共领域。允许商业和非商业用途。
订阅并加入 Medium
请订阅以便在我发布新故事时获得通知。
如果你还不是会员,你可以通过推荐链接加入 Medium。
你可能还会喜欢
你可能还会喜欢以下内容:
点击这里了解更多关于 GPT 与 BERT 的信息 [## GPT 与 BERT:哪个更好?]
比较两个大型语言模型:方法和示例
towardsdatascience.com
额外资源
网站
你可以访问我的网站进行零编码分析。https://experiencedatascience.com
YouTube 频道
请访问我的 YouTube 频道,通过演示学习数据科学和人工智能应用案例。
点击这里查看数据科学演示 [## 数据科学演示]
通过演示学习数据科学。不论你从事哪个职业,都可以放松身心,享受视频。我的名字是……
使用 H2O LLM Studio 对 LLM 模型进行微调以生成 Cypher 语句
避免依赖外部和不断变化的 API 来构建你的知识图谱聊天机器人
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 4 月 24 日
--
照片由 Mike Hindle 拍摄,来自 Unsplash
像 ChatGPT 这样的大型语言模型有一个知识截止日期,之后它们不了解任何发生的事件。与其用更晚的信息微调模型,不如在查询时为 LLM 提供额外的外部上下文。我已经撰写了一些博客文章,介绍了实现 基于知识图谱的上下文感知聊天机器人 和 一个能够阅读公司资源以回答问题的聊天机器人。然而,到目前为止,我在所有示例中使用的都是 OpenAI 的大型语言模型。
尽管 OpenAI 官方立场是他们不使用用户数据来改进他们的模型,但有像 三星员工通过输入 ChatGPT 泄露机密数据 这样的故事。如果我处理的是绝密的专有信息,我会保持谨慎,不与 OpenAI 分享这些信息。幸运的是,新开源的 LLM 模型每天都在涌现。
我测试了许多开源 LLM 模型,以评估它们生成 Cypher 语句的能力。它们中的一些对 Cypher 语法有基本了解。然而,我还没有找到任何模型能够根据提供的示例或图谱模式可靠地生成 Cypher 语句。因此,唯一的解决方案是微调一个开源 LLM 模型,以可靠地生成 Cypher 语句。
我从未微调过任何 NLP 模型,更不用说 LLM 了。因此,我必须找到一种简单的方法来入门,而无需首先获得机器学习博士学位。幸运的是,我偶然发现了 H2O 最近发布的 LLM Studio 工具,它提供了一个图形界面用于微调 LLM 模型。我很高兴地发现,微调 LLM 不再需要我编写任何代码或长命令。只需几次鼠标点击,我就能完成任务。
[## GitHub - h2oai/h2o-llmstudio: H2O LLM Studio - 一个用于微调 LLM 的框架和无代码 GUI
欢迎来到 H2O LLM Studio,一个用于微调最先进的大型语言模型的框架和无代码 GUI…
github.com](https://github.com/h2oai/h2o-llmstudio?source=post_page-----3f34822ad5--------------------------------)
本博客文章中的所有代码均 在 GitHub 上可用。
准备训练数据集
首先,我必须学习训练数据集的结构应如何安排。我查看了他们的 教程笔记本,发现该工具可以处理以 CSV 文件提供的训练数据,其中第一列包括用户提示,第二列包含期望的 LLM 响应。
好的,这很简单。现在我只需生成训练示例。我决定 200 个训练示例是一个好数字。然而,我实在懒得手动编写 200 个 Cypher 语句。因此,我使用了 GPT-4 来完成这项工作。代码可以在这里找到:
[## blogs/LLM_train_dataset.ipynb at master · tomasonjo/blogs
目前无法执行该操作。您在另一个标签或窗口中登录。您在另一个标签或窗口中退出了…
电影推荐数据集已嵌入到 GPT-4 中,因此它可以生成足够好的示例。然而,一些示例略有偏差,不符合图表模式。因此,如果我为商业用途微调 LLM,我会使用 GPT-4 生成 Cypher 语句,然后手动检查以验证它们。此外,我还要确保验证集不包含训练集中的示例。
我还测试了是否需要“为以下问题创建一个 Cypher 语句”作为指令的前缀。似乎一些模型如 EleutherAI/pythia-12b-deduped 需要这个前缀,否则效果会非常差。另一方面,facebook/opt-13b 即使没有前缀也表现得很好。
没有前缀或有前缀的指令训练的模型。图片由作者提供。
为了能够使用相同的数据集比较所有模型,我使用了一个数据集,该数据集在数据集的指令部分添加了前缀“为以下问题创建一个 Cypher 语句:”。
H2O LLM Studio 安装
H2O LLM Studio 可以通过两个简单步骤进行安装。第一步是安装 Python 3.10 环境,如果它缺失的话。安装 Python 3.10 的步骤描述在他们的 GitHub 仓库中。
[## GitHub - h2oai/h2o-llmstudio: H2O LLM Studio - 用于微调 LLM 的框架和无代码 GUI
欢迎使用 H2O LLM Studio,一个为微调最先进大型语言模型设计的框架和无代码 GUI…
github.com](https://github.com/h2oai/h2o-llmstudio?source=post_page-----3f34822ad5--------------------------------)
在确保有 Python 3.10 环境后,我们只需克隆仓库并使用 make install
命令安装依赖项。安装完成后,我们可以使用 make wave
命令运行 LLM studio。现在可以通过在浏览器中打开 localhost:10101
网站来打开图形界面。
H2O LLM Studio 首页。图片由作者提供。
导入数据集
首先,我们必须导入用于微调 LLM 的数据集。如果你不想创建自己的数据集,可以下载我使用的那个。请注意,它不是经过整理的,一些示例不符合电影推荐图表模式。然而,这是了解工具的一个很好的起点。我们可以使用拖放界面导入 CSV 文件。
上传 CSV 界面。图片由作者提供。
这有点反直觉,但我们必须分别上传训练集和验证集。假设我们首先上传训练集。然后,当我们上传验证集时,我们必须使用合并数据集选项,以便在同一个数据集中包含训练集和验证集。
导入的数据集包含训练和验证数据框。图片由作者提供。
最终的数据集应该包含训练和验证的数据框。
我了解到,你还可以上传一个包含训练集和验证集的 ZIP 文件,以避免分别上传文件。
创建实验
现在一切准备就绪,我们可以继续微调 LLM 模型。如果我们点击创建实验选项卡,就会看到微调选项。最重要的设置包括用于训练的数据集、LLM 骨干,我在实验中也增加了epochs的数量。其他参数我保持默认,因为我不清楚它们的作用。我们可以从 13 个 LLM 模型中进行选择:
可用的 LLM 模型。图片由作者提供。
请注意,参数数量越高,我们需要的 GPU RAM 就越多用于微调和推理。例如,我在使用 40GB GPU 微调一个具有 20B 参数的 LLM 模型时内存不足。另一方面,我们期望 LLM 的参数数量越高,结果会更好。我认为,对于像 pythia-1b 这样的小型 LLM,我们需要大约 5GB 的 GPU RAM,而对于 opt-13b 模型则需要最多 40GB 的 GPU。设置好所需参数后,我们可以通过单击一次来运行实验。在大多数情况下,使用 Nvidia A100 40GB 进行微调的过程相对较快。
实验页面。图片由作者提供。
大多数模型在使用 15 个 epochs 的情况下,训练时间不到 30 分钟。LLM Studio 的一个优点是它会生成一个仪表板来检查训练结果。
LLM 微调指标。图片由作者提供。
不仅如此,我们还可以在图形界面中与模型进行聊天。
LLM Studio 中的聊天界面。图片由作者提供。
将模型导出到 HuggingFace 仓库
就在 H2O LLM Studio 已经够酷的情况下,它还允许通过单击将微调过的模型导出到 HuggingFace。
将模型导出到 HuggingFace。图片由作者提供。
能够通过单击将模型导出到 HuggingFace 库,使我们可以在工作流中尽可能轻松地使用该模型。我已经导出了一种可以在 Google Colab 中运行的小型微调 pythia-1b 模型,以演示如何与 transformers 库一起使用。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = "cuda:0" if torch.cuda.is_available() else "cpu"
tokenizer = AutoTokenizer.from_pretrained("tomasonjo/movie-generator-small")
model = AutoModelForCausalLM.from_pretrained("tomasonjo/movie-generator-small").to(
device
)
prefix = "\nCreate a Cypher statement to answer the following question:"
def generate_cypher(prompt):
inputs = tokenizer(
f"{prefix}{prompt}<|endoftext|>", return_tensors="pt", add_special_tokens=False
).to(device)
tokens = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.3,
repetition_penalty=1.2,
num_beams=4,
)[0]
tokens = tokens[inputs["input_ids"].shape[1] :]
return tokenizer.decode(tokens, skip_special_tokens=True)
LLM Studio 使用一个特殊的<|endoftext|>
字符,该字符必须添加到用户提示的末尾,以确保模型正确运行。因此,在使用调整过的模型时,我们也必须在 transformers 库中执行相同操作。除此之外,实际上没有其他需要做的事情。我们现在可以使用模型生成 Cypher 语句。
generate_cypher("How many movies did Tom Hanks appear in?")
#MATCH (d:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie)
#RETURN {movie: m.title} AS result
generate_cypher("When was Toy Story released?")
#MATCH (m:Movie {title: 'When'})-[:IN_GENRE]->(g:Genre)
#RETURN {genre: g.name} AS result
我故意展示了一个有效和一个无效的 Cypher 语句,以说明较小的模型可能足够用于演示,其中提示可以预定义。另一方面,你可能不愿意在生产中使用它们。然而,使用更大的模型会有代价。例如,要运行具有 12B 参数的模型,我们至少需要 24 GB 的 GPU,而 20B 参数模型则需要 48 GB 的 GPU。
摘要
微调开源 LLMs 使我们能够摆脱对 OpenAI 的依赖。虽然 GPT-4 表现更好,特别是在需要提问的对话环境中,但我们仍然可以将我们的绝密数据保留给自己。在写这篇博客文章时,我测试了多个模型,除了由于 GPU 内存问题而没有测试 20B 模型。我可以自信地说,你可以微调一个模型以生成足够好的 Cypher 语句用于生产设置。需要注意的一点是,涉及到模型需要依赖前面对话来理解问题背景的后续问题,目前似乎无法正常工作。因此,我们被限制在单步查询中,需要在单个提示中提供完整的上下文。然而,随着开源 LLMs 的发展迅猛,我对未来的发展充满期待。
直到那时,如果你想通过仅需几次点击就能调整一个 LLM 以适应个人或公司的需求,可以试试H2O LLM Studio。
微调大型语言模型(LLMs)
原文:
towardsdatascience.com/fine-tuning-large-language-models-llms-23473d763b91
一个带有示例 Python 代码的概念概述
·发表于Towards Data Science ·14 分钟阅读·2023 年 9 月 11 日
--
这是关于使用大型语言模型(LLMs)实践的第 5 篇文章。在这篇文章中,我们将讨论如何对预训练的 LLM 进行微调(FT)。我们将首先介绍关键的 FT 概念和技术,然后通过一个具体的示例,演示如何使用 Python 和 Hugging Face 的软件生态系统在本地微调模型。
调整语言模型。图像由作者提供。
在本系列的上一篇文章中,我们看到如何通过将提示工程整合到 Python 代码中来构建实用的 LLM 驱动应用程序。对于绝大多数 LLM 使用案例,这是我推荐的初步方法,因为它比其他方法需要的资源和技术专长少得多,同时仍然能提供许多好处。
然而,存在一些情况,其中直接提示现有的 LLM 并不够有效,需要更复杂的解决方案。这就是模型微调可以发挥作用的地方。
附加视频。
什么是微调?
微调是对一个预训练的模型进行至少一个内部模型参数的训练(即权重)。在 LLMs 的上下文中,这通常是将一个通用的基础模型(例如 GPT-3)转变为一个针对特定用例的专业模型(例如 ChatGPT)[1]。
这种方法的主要优势在于,与仅依赖监督训练的模型相比,模型可以在需要(远)少得多的人工标记示例的情况下实现更好的性能。
虽然严格的自监督基础模型在借助提示工程[2]的帮助下可以在各种任务上展现出令人印象深刻的表现,但它们仍然是词预测器,可能生成的完成结果并不完全有用或准确。例如,让我们比较 davinci(基础 GPT-3 模型)和 text-davinci-003(一个经过微调的模型)的完成结果。
davinci(基础 GPT-3 模型)和 text-davinci-003(一个经过微调的模型)的完成结果比较。图片由作者提供。
注意,基础模型仅仅是通过列出一系列问题来完成文本,就像谷歌搜索或作业任务一样,而微调模型提供了更有帮助的响应。text-davinci-003 所使用的微调方法是对齐微调,其目的是使大型语言模型的响应更有帮助、更诚实和无害,但稍后会详细介绍[3,4]。
为什么要微调
微调不仅能提高基础模型的性能,而且较小的(微调的)模型在训练的任务集上往往能超越更大的(更昂贵的)模型[4]。OpenAI 通过其第一代“InstructGPT”模型证明了这一点,其中 1.3B 参数的 InstructGPT 模型在完成结果上优于 175B 参数的 GPT-3 基础模型,尽管它小了 100 倍[4]。
虽然我们今天可能与之互动的大多数大型语言模型并非像 GPT-3 那样严格的自监督模型,但对现有的微调模型进行特定用例的提示仍然存在缺陷。
一个重要的问题是,大型语言模型有一个有限的上下文窗口。因此,模型可能在需要大量知识基础或特定领域信息的任务上表现不佳[1]。微调模型可以通过在微调过程中“学习”这些信息来避免这个问题。这也避免了在提示中塞入额外的上下文,从而可以降低推理成本。
微调的三种方法
有三种通用的模型微调方法:自监督、监督和强化学习。这些方法并非相互排斥,可以按顺序组合使用这三种方法来微调一个模型。
自监督学习
自监督学习包括基于训练数据的固有结构来训练模型。在大型语言模型的背景下,这通常表现为给定一个词(或更准确地说是令牌)的序列,预测下一个词(令牌)。
尽管这是许多现今预训练语言模型的开发方式,但它也可以用于模型微调。一个潜在的应用场景是开发一个能够模拟某人写作风格的模型,给定一组示例文本。
监督学习
下一种,也许是最流行的,微调模型的方法是通过监督学习。这涉及到对特定任务的输入-输出对进行模型训练。一个例子是指令调整,其目的是提升模型在回答问题或回应用户提示时的表现[1,3]。
监督学习中的关键步骤是策划训练数据集。一种简单的方法是创建问答对并将其整合到提示模板中[1,3]。例如,问答对:谁是美国第 35 任总统?—— 约翰·F·肯尼迪 可以粘贴到下面的提示模板中。更多示例提示模板可以在参考文献[4]的 A.2.1 节中找到。
"""Please answer the following question.
Q: {Question}
A: {Answer}"""
使用提示模板很重要,因为像 GPT-3 这样的基础模型本质上是“文档补全器”。这意味着,给定一些文本,模型会生成在该上下文中(统计上)有意义的更多文本。这回到本系列的上一篇博客和通过提示工程“欺骗”语言模型来解决问题的想法。
7 个提示技巧、Langchain 和 Python 示例代码
towardsdatascience.com
强化学习
最后,可以使用强化学习(RL)来微调模型。RL使用奖励模型来指导基础模型的训练。这可以采取多种形式,但基本思想是训练奖励模型以对语言模型的完成情况进行评分,使其反映人类标注者的偏好[3,4]。然后,奖励模型可以与强化学习算法(例如,近端策略优化(PPO))结合使用,以微调预训练模型。
OpenAI 的 InstructGPT 模型展示了如何使用强化学习进行模型微调,这些模型是通过3 个关键步骤开发的[4]。
-
生成高质量的提示-响应对,并使用监督学习微调预训练模型。(~13k 训练提示) 注意:可以(可选地)跳到步骤 2,使用预训练模型[3]。
-
使用微调后的模型生成完成,并让人工标注者根据其偏好对响应进行排名。使用这些偏好来训练奖励模型。(~33k 训练提示)
-
使用奖励模型和强化学习算法(例如 PPO)进一步微调模型。(~31k 训练提示)
尽管上述策略通常会导致 LLM 完成度显著优于基础模型,但这也可能导致在某些任务上的性能下降。这种性能下降也被称为对齐成本[3,4]。
监督微调步骤(高级)
如上所述,可以通过许多方法微调现有语言模型。然而,本文剩余部分将专注于通过监督学习进行微调。以下是监督模型微调的高级程序[1]。
-
选择微调任务(例如总结、问答、文本分类)
-
准备训练数据集即创建(100–10k)输入输出对并预处理数据(即标记化、截断和填充文本)。
-
选择基础模型(尝试不同模型并选择在所需任务上表现最佳的一个)。
-
通过监督学习微调模型
-
评估模型性能
虽然这些步骤中的每一个都可以成为一篇文章,但我想专注于第 4 步并讨论如何训练微调后的模型。
参数训练的 3 种选项
在微调拥有~100M-100B 参数的模型时,需要考虑计算成本。在这方面,一个重要的问题是——我们(重新)训练哪些参数?
在处理大量参数时,我们有无数的选择来决定训练哪些参数。在这里,我将专注于三种通用选项。
选项 1:重新训练所有参数
第一个选项是训练所有内部模型参数(称为完全参数调优)[3]。虽然这个选项在概念上很简单,但它是最耗费计算资源的。此外,完全参数调优的一个已知问题是灾难性遗忘现象。这是模型“遗忘”了在初始训练中“学到”的有用信息[3]。
我们可以通过冻结模型参数的大部分来缓解选项 1 的缺点,这就引出了选项 2。
选项 2:迁移学习
迁移学习(TL)的核心思想是保留模型从过去训练中学到的有用表示/特征,并将模型应用于新任务时使用。这通常包括“去掉神经网络(NN)的头部并用新的头部替换它”(例如,添加具有随机权重的新层)。注意:神经网络的头部包括其最终层,这些层将模型的内部表示转换为输出值。
虽然保持大部分参数不变可以减轻训练 LLM 的巨大计算成本,但 TL 可能无法解决灾难性遗忘问题。为了更好地处理这两个问题,我们可以转向一组不同的方法。
选项 3:参数高效微调(PEFT)
PEFT 涉及 用相对少量的可训练参数增强基础模型。其关键结果是一种微调方法,能够在极低的计算和存储成本下展示出与完全参数调整相当的性能 [5]。
PEFT 封装了一系列技术,其中之一是流行的 LoRA (低秩适配) 方法 [6]。LoRA 的基本思想是选择现有模型中的一部分层,并根据以下方程式修改它们的权重。
方程式显示了如何使用 LoRA 修改权重矩阵以进行微调 [6]。图片由作者提供。
其中 h() = 一个将被调整的隐藏层,x = 输入到 h() 的数据,W₀ = h 的原始权重矩阵,而 ΔW = 注入到 h 的可训练参数矩阵。ΔW 根据 ΔW=BA 被分解,其中 ΔW 是一个 d 乘 k 的矩阵,B 是 d 乘 r 的矩阵,而 A 是 r 乘 k 的矩阵。r 是 ΔW 的假定“内在秩”(可以小到 1 或 2)[6]。
对不起有这么多数学公式,但关键点是 W₀ 中的 (d * k) 权重是冻结的,因此不包括在优化中。相反,构成矩阵 B 和 A 的 ((d * r) + (r * k)) 权重是唯一经过训练的。
通过将一些虚构的数字代入 d=100, k=100 和 r=2 来感受效率提升,可训练参数的数量从 10,000 降到 400。实际上,LoRA 论文的作者提到,使用 LoRA 微调 GPT-3 相比完全参数调整,参数检查点大小减少了 10,000 倍 [6]。
为了使这更加具体,让我们看看如何使用 LoRA 高效地微调语言模型,以便在个人计算机上运行。
示例代码:使用 LoRA 微调 LLM
在这个示例中,我们将使用 Hugging Face 生态系统来微调语言模型,以将文本分类为“正面”或“负面”。在这里,我们微调 distilbert-base-uncased,一个基于 BERT 的约 70M 参数模型。由于这个基础模型是为了语言建模而不是分类训练的,我们采用 迁移学习 将基础模型头部替换为分类头。此外,我们使用 LoRA 高效地微调模型,使其可以在我的 Mac Mini(M1 芯片,16GB 内存)上在合理时间内(约 20 分钟)运行。
代码以及 conda 环境文件可以在 GitHub 仓库 中找到。最终模型和 数据集 [7] 可在 Hugging Face 上获得。
[## YouTube-Blog/LLMs/fine-tuning at main · ShawhinT/YouTube-Blog
代码以补充 YouTube 视频和 Medium 上的博客文章。 - YouTube-Blog/LLMs/fine-tuning at main ·…
导入
我们从导入有用的库和模块开始。Datasets、transformers、peft 和 evaluate 都是来自Hugging Face (HF) 的库。
from datasets import load_dataset, DatasetDict, Dataset
from transformers import (
AutoTokenizer,
AutoConfig,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
TrainingArguments,
Trainer)
from peft import PeftModel, PeftConfig, get_peft_model, LoraConfig
import evaluate
import torch
import numpy as np
基础模型
接下来,我们加载基础模型。这里的基础模型相对较小,但我们还可以使用其他几个(更大的)模型(例如 roberta-base、llama2、gpt2)。完整列表请参见这里。
model_checkpoint = 'distilbert-base-uncased'
# define label maps
id2label = {0: "Negative", 1: "Positive"}
label2id = {"Negative":0, "Positive":1}
# generate classification model from model_checkpoint
model = AutoModelForSequenceClassification.from_pretrained(
model_checkpoint, num_labels=2, id2label=id2label, label2id=label2id)
加载数据
然后,我们可以从 HF 的数据集库中加载我们的训练和验证数据。这是一个包含 2000 条电影评论(1000 条用于训练,1000 条用于验证)的数据集,其中的二元标签表示评论是积极的(还是消极的)。
# load dataset
dataset = load_dataset("shawhin/imdb-truncated")
dataset
# dataset =
# DatasetDict({
# train: Dataset({
# features: ['label', 'text'],
# num_rows: 1000
# })
# validation: Dataset({
# features: ['label', 'text'],
# num_rows: 1000
# })
# })
预处理数据
接下来,我们需要预处理数据,以便用于训练。这包括使用分词器将文本转换为基础模型可以理解的整数表示。
# create tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, add_prefix_space=True)
要将分词器应用于数据集,我们使用.map()方法。这需要一个自定义函数来指定文本应如何预处理。在这种情况下,该函数称为tokenize_function()。除了将文本转换为整数之外,此函数还会截断整数序列,使其不超过 512 个数字,以符合基础模型的最大输入长度。
# create tokenize function
def tokenize_function(examples):
# extract text
text = examples["text"]
#tokenize and truncate text
tokenizer.truncation_side = "left"
tokenized_inputs = tokenizer(
text,
return_tensors="np",
truncation=True,
max_length=512
)
return tokenized_inputs
# add pad token if none exists
if tokenizer.pad_token is None:
tokenizer.add_special_tokens({'pad_token': '[PAD]'})
model.resize_token_embeddings(len(tokenizer))
# tokenize training and validation datasets
tokenized_dataset = dataset.map(tokenize_function, batched=True)
tokenized_dataset
# tokenized_dataset =
# DatasetDict({
# train: Dataset({
# features: ['label', 'text', 'input_ids', 'attention_mask'],
# num_rows: 1000
# })
# validation: Dataset({
# features: ['label', 'text', 'input_ids', 'attention_mask'],
# num_rows: 1000
# })
# })
此时,我们还可以创建一个数据整理器,它将在训练期间动态填充每个批次中的示例,以使它们都具有相同的长度。这比将所有示例填充到整个数据集中的相同长度更具计算效率。
# create data collator
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
评估指标
我们可以通过自定义函数定义如何评估我们微调后的模型。在这里,我们定义了compute_metrics()函数来计算模型的准确率。
# import accuracy evaluation metric
accuracy = evaluate.load("accuracy")
# define an evaluation function to pass into trainer later
def compute_metrics(p):
predictions, labels = p
predictions = np.argmax(predictions, axis=1)
return {"accuracy": accuracy.compute(predictions=predictions,
references=labels)}
未训练模型的性能
在训练模型之前,我们可以评估基础模型在随机初始化分类头上的一些示例输入的表现。
# define list of examples
text_list = ["It was good.", "Not a fan, don't recommed.",
"Better than the first one.", "This is not worth watching even once.",
"This one is a pass."]
print("Untrained model predictions:")
print("----------------------------")
for text in text_list:
# tokenize text
inputs = tokenizer.encode(text, return_tensors="pt")
# compute logits
logits = model(inputs).logits
# convert logits to label
predictions = torch.argmax(logits)
print(text + " - " + id2label[predictions.tolist()])
# Output:
# Untrained model predictions:
# ----------------------------
# It was good. - Negative
# Not a fan, don't recommed. - Negative
# Better than the first one. - Negative
# This is not worth watching even once. - Negative
# This one is a pass. - Negative
正如预期的那样,模型的性能等同于随机猜测。让我们看看如何通过微调来改善这一点。
使用 LoRA 微调
要使用 LoRA 进行微调,我们首先需要一个配置文件。这设置了 LoRA 算法的所有参数。有关更多详细信息,请参见代码块中的注释。
peft_config = LoraConfig(task_type="SEQ_CLS", # sequence classification
r=4, # intrinsic rank of trainable weight matrix
lora_alpha=32, # this is like a learning rate
lora_dropout=0.01, # probablity of dropout
target_modules = ['q_lin']) # we apply lora to query layer only
然后,我们可以创建一个新的模型版本,该模型可以通过 PEFT 进行训练。注意到可训练参数的规模减少了大约 100 倍。
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# trainable params: 1,221,124 || all params: 67,584,004 || trainable%: 1.8068239934408148
接下来,我们定义模型训练的超参数。
# hyperparameters
lr = 1e-3 # size of optimization step
batch_size = 4 # number of examples processed per optimziation step
num_epochs = 10 # number of times model runs through training data
# define training arguments
training_args = TrainingArguments(
output_dir= model_checkpoint + "-lora-text-classification",
learning_rate=lr,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
num_train_epochs=num_epochs,
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
最后,我们创建一个 trainer()对象并微调模型!
# creater trainer object
trainer = Trainer(
model=model, # our peft model
args=training_args, # hyperparameters
train_dataset=tokenized_dataset["train"], # training data
eval_dataset=tokenized_dataset["validation"], # validation data
tokenizer=tokenizer, # define tokenizer
data_collator=data_collator, # this will dynamically pad examples in each batch to be equal length
compute_metrics=compute_metrics, # evaluates model using compute_metrics() function from before
)
# train model
trainer.train()
上述代码将在训练期间生成以下指标表。
模型训练指标。图片由作者提供。
训练模型性能
为了查看模型性能的提升,我们来将其应用于之前的相同 5 个示例。
model.to('mps') # moving to mps for Mac (can alternatively do 'cpu')
print("Trained model predictions:")
print("--------------------------")
for text in text_list:
inputs = tokenizer.encode(text, return_tensors="pt").to("mps") # moving to mps for Mac (can alternatively do 'cpu')
logits = model(inputs).logits
predictions = torch.max(logits,1).indices
print(text + " - " + id2label[predictions.tolist()[0]])
# Output:
# Trained model predictions:
# ----------------------------
# It was good. - Positive
# Not a fan, don't recommed. - Negative
# Better than the first one. - Positive
# This is not worth watching even once. - Negative
# This one is a pass. - Positive # this one is tricky
微调后的模型相较于之前的随机猜测有了显著改善,正确分类了上述代码中的所有示例,除了一个。这与我们在训练过程中看到的约 90%准确率指标相符。
结论
虽然微调现有模型比使用开箱即用的模型需要更多的计算资源和技术专长,(较小的)微调模型可以在特定用例中超过(较大的)预训练基础模型,即使在使用巧妙的提示工程策略时也是如此。此外,借助所有可用的开源 LLM 资源,微调模型以满足自定义应用从未如此简单。
本系列的下一篇文章将超越模型微调,讨论如何从头开始训练一个语言模型。
👉 更多关于 LLMs: 介绍 | OpenAI API | Hugging Face Transformers | 提示工程 | 构建 LLM | QLoRA | RAG | 文本嵌入
大型语言模型(LLMs)
查看列表13 个故事
资源
社交媒体: YouTube 🎥 | LinkedIn | Twitter
支持: 请我喝咖啡 ☕️
免费获取我写的每个新故事 P.S. 我不会将您的邮件分享给任何人 注册即创建一个…
shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----23473d763b91--------------------------------)
[1] Deeplearning.ai 大型语言模型微调短课程: www.deeplearning.ai/short-courses/finetuning-large-language-models/
[2] arXiv:2005.14165 [cs.CL] (GPT-3 论文)
[3] arXiv:2303.18223 [cs.CL] (LLMs 综述)
[4] arXiv:2203.02155 [cs.CL] (InstructGPT 论文)
[5] 🤗 PEFT: 在低资源硬件上对亿级规模模型进行参数高效微调: huggingface.co/blog/peft
[6] arXiv:2106.09685 [cs.CL] (LoRA 论文)
[7] 原始数据集来源 — Andrew L. Maas, Raymond E. Daly, Peter T. Pham, Dan Huang, Andrew Y. Ng, 和 Christopher Potts. 2011. 学习词向量进行情感分析。在第 49 届计算语言学协会年会:人类语言技术会议论文集,第 142–150 页,美国俄勒冈州波特兰市。计算语言学协会。
使用 MNR 损失微调句子变换器
原文:
towardsdatascience.com/fine-tuning-sentence-transformers-with-mnr-loss-cd6a26685b81
语义搜索的 NLP
多负样本排名损失的下一代句子嵌入
·发表于Towards Data Science ·8 分钟阅读·2023 年 2 月 3 日
--
图片由Lars Kienle提供,来源于Unsplash。文章最初发布于 pinecone.io,作者在该网站工作。
Transformer 生成的句子嵌入在很短的时间内取得了长足的进展。从 BERT 交叉编码器的缓慢但准确的相似度预测开始,句子嵌入的世界在 2019 年随着 SBERT 的出现而被点燃[1]。自那时以来,许多新的句子变换器被引入,这些模型迅速使原始 SBERT 成为过时的技术。
这些新型句子变换器是如何如此迅速超越 SBERT 的?答案是多负样本排名(MNR)损失。
本文将介绍 MNR 损失是什么,它所需的数据,以及如何实施以微调我们自己高质量的句子变换器。
实施将涵盖两种训练方法。第一种方法更为复杂,详细说明了微调模型的具体步骤。第二种方法则利用了sentence-transformers
库出色的微调工具。
NLI 训练
我们关于softmax 损失的文章解释了如何使用自然语言推理(NLI)数据集来微调句子变换器。
这些数据集包含许多句子对,有些句子对暗示彼此,而其他句子对不暗示彼此。与 softmax 损失文章一样,我们将使用两个数据集:斯坦福自然语言推理(SNLI)和多领域 NLI(MNLI)语料库。
这两个语料库总共有 943K 句子对。每对包括一个premise
和一个hypothesis
句子,并且被分配一个label
:
-
0 — 蕴涵,例如,
premise
暗示了hypothesis
。 -
1 — 中立,
premise
和hypothesis
可能都是真的,但它们不一定相关。 -
2 — 矛盾,
premise
和hypothesis
相互矛盾。
在使用 MNR 损失进行微调时,我们将丢弃所有标记为中立或矛盾的行——仅保留正样本的蕴涵对。
我们将每一步将句子 A(premise
,称为锚点)和句子 B(hypothesis
,当标签为0时,这称为正样本)输入 BERT。与 softmax 损失不同,我们不使用label
特征。
这些训练步骤是分批进行的。这意味着同时处理多个锚点-正样本对。
然后对模型进行优化,以在对之间生成相似的嵌入,同时对非对保持不同的嵌入。我们会很快更深入地解释这一点。
数据准备
让我们来看看数据准备过程。我们首先需要下载并合并两个 NLI 数据集。我们将使用 Hugging Face 的datasets
库。
由于我们使用的是 MNR 损失,我们只需要锚点-正样本对。我们可以应用过滤器来移除所有其他对(包括错误的-1
标签)。
数据集的准备方式现在取决于我们的训练方法。我们将继续准备更复杂的 PyTorch 方法。如果你更愿意直接训练模型而不关心涉及的步骤,可以跳过到下一部分。
对于 PyTorch 方法,我们必须对自己的数据进行分词。为此,我们将使用transformers
库中的BertTokenizer
并在我们的dataset
上应用map
方法。
完成这些后,我们就准备初始化我们的DataLoader
,它将在训练期间用于将数据批次加载到模型中。
这样,我们的数据就准备好了。让我们继续训练。
PyTorch 微调
训练 SBERT 模型时,我们不是从零开始,而是从已经预训练的 BERT 开始——我们只需微调它来构建句子嵌入。
MNR 和 softmax 损失训练方法在微调过程中使用‘siamese’–BERT 架构。这意味着在每一步中,我们将句子 A(我们的锚点)输入 BERT,然后是句子 B(我们的正样本)。
Siamese-BERT 网络中,锚点和正样本句子对被分别处理。一个均值池化层将 token 嵌入转换为句子嵌入。句子 A 是我们的锚点,句子 B 是正样本。
因为这两个句子是分别处理的,所以它创建了一个类似孪生的网络,两个相同的 BERT 并行训练。实际上,每一步只使用一个 BERT。
我们可以进一步扩展到三元组网络。对于 MNR 的三元组网络,我们会传递三个句子,一个锚点,一个正样本和一个负样本。然而,我们没有使用三元组网络,因此我们已从数据集中移除了负样本行(label
为 2
的行)。
Triplet 网络使用相同的逻辑,但增加了一个句子。对于 MNR 损失,这个句子是负样本对的锚点。
BERT 输出 512 个 768 维的嵌入。我们通过均值池化将这些嵌入转换为平均句子嵌入。使用孪生网络方法,每一步生成两个这样的嵌入——一个是我们称之为a
的锚点,另一个是称之为p
的正样本。
在mean_pool
函数中,我们取这些 token 级别的嵌入(512)和句子 attention_mask
张量。我们调整attention_mask
以匹配 token 嵌入的更高 768
维度。
调整后的掩码in_mask
应用于 token 嵌入,以排除均值池化操作中的填充 token。均值池化计算每个维度的值的平均激活,但排除那些填充值,以免减少平均激活。此操作将我们的 token 级别嵌入(形状为512*768
)转换为句子级别嵌入(形状为1*768
)。
这些步骤是在批次中执行的,即我们同时处理许多(锚点,正样本)对。这在接下来的步骤中很重要。
首先,我们计算每个锚点嵌入(a
)与同一批次中所有正样本嵌入(p
)之间的余弦相似度。
从这里,我们为每个锚点嵌入a_i
生成一个余弦相似度分数向量(大小为batch_size
)(或大小为*2 * batch_size*
的三元组)。每个锚点应与其正样本p_i
共享最高分数。
使用五对/三元组的三元组网络进行余弦相似度评分(使用 (a, p, n)
)。孪生网络相同,但排除了深蓝色的 n
块(n
)。
为了优化,我们使用一组递增的标签值来标记每个a_i
的最高分数位置,并使用分类 交叉熵损失。
这就是我们用于 MNR 损失微调所需的所有组件。让我们把这些整合起来,设置一个训练循环。首先,我们将模型和层移动到支持 CUDA 的 GPU (如果有的话)。
然后我们设置优化器和训练计划。我们使用 Adam 优化器,并对总步数的 10% 进行线性预热。
现在,我们使用相同的训练过程定义训练循环。
就这样,我们使用 MNR 损失微调了我们的 BERT 模型。现在我们将其保存到文件中。
现在可以使用SentenceTransformer
或 HF from_pretrained
方法加载它。在我们开始测试模型性能之前,让我们看看如何使用更简单的 sentence-transformers
库来复制微调逻辑。
快速微调
如前所述,使用 MNR 损失微调模型有一个更简单的方法。sentence-transformers
库允许我们使用预训练的句子变换器,并附带一些方便的训练工具。
我们将从预处理数据开始。这与我们之前进行的前几步相同。
之前,我们将数据标记化,然后加载到 PyTorch DataLoader
中。这次我们遵循稍微不同的格式。我们不标记化;我们重新格式化为sentence-transformers
InputExample
对象的列表,并使用略有不同的DataLoader
。
我们的InputExample
仅包含我们的a
和p
句子对,然后将其输入到NoDuplicatesDataLoader
对象中。该数据加载器确保每个批次没有重复——这是在使用 MNR 损失对随机抽样对进行排名时的一个有用功能。
现在我们定义模型。sentence-transformers
库允许我们使用模块来构建模型。我们只需要一个变换器模型(我们将再次使用bert-base-uncased
)和一个平均池化模块。
我们现在有了一个初始化的模型。在训练之前,剩下的就是损失函数——MNR 损失。
这样,我们的数据加载器、模型和损失函数就准备好了。剩下的就是微调模型!与之前一样,我们将训练一个周期,并在训练步骤的前 10%进行热身。
几个小时后,我们有了一个使用 MNR 损失训练的新句子变换器模型。毫无疑问,使用sentence-transformers
训练工具使生活轻松得多。为了结束这篇文章,让我们看看我们的 MNR 损失 SBERT 与其他句子变换器的性能。
比较句子变换器
我们将使用语义文本相似性(STS)数据集来测试四个模型的性能;我们的MNR 损失 SBERT(使用 PyTorch 和sentence-transformers
)、原始 SBERT,以及一个在1B+样本数据集上用 MNR 损失训练的 MPNet 模型。
首先我们需要下载 STS 数据集。我们将再次使用 Hugging Face 的datasets
。
STSb(或 STS 基准)包含特征sentence1
和sentence2
中的句子对,并分配了从0 -> 5的相似性分数。
STSb 验证集中的三个样本。
由于相似性分数范围为 0 -> 5,我们需要将其归一化到 0 -> 1 的范围。我们使用map
来完成这项工作。
我们将使用sentence-transformers
的评估工具。我们首先需要使用InputExample
类重新格式化 STSb 数据——将句子特征作为texts
,将相似度分数传递给label
参数。
要评估模型,我们需要初始化适当的评估对象。由于我们在评估连续相似度分数,我们使用EmbeddingSimilarityEvaluator
。
这样,我们就准备好开始评估了。我们将模型加载为SentenceTransformer
对象,并将模型传递给我们的evaluator
。
评估器输出斯皮尔曼等级相关系数,用于计算模型输出嵌入与 STSb 提供的相似度分数之间的余弦相似度。两个值之间的高相关性输出接近+1,没有相关性则输出0。
对于使用sentence-transformers
微调的模型,我们输出了0.84的相关性,这意味着我们的模型根据 STSb 分配的分数输出了良好的相似度分数。让我们将其与其他模型进行比较。
排名前两的模型使用 MNR 损失训练,其次是原始 SBERT。
这些结果支持了sentence-transformers
作者给出的建议,即使用 MNR 损失训练的模型在构建高性能句子嵌入方面优于使用 softmax 损失训练的模型[2]。
另一个关键点是,尽管我们尽了最大努力,并且构建这些 PyTorch 模型的复杂性很高,但每个使用易用的sentence-transformers
工具训练的模型都远远超越了它们。
简而言之,用 MNR 损失微调你的模型,并使用sentence-transformers
库进行微调。
这就是本次关于使用多负样本排名损失微调句子变换器模型的教程和指南——当前构建高性能模型的最佳方法。
我们首先处理了两个最受欢迎的 NLI 数据集——斯坦福 NLI 和多类别 NLI 语料库——以便使用 MNR 损失进行微调。随后,我们深入探讨了使用 PyTorch 进行这种微调方法的细节,然后利用了sentence-transformers
库提供的出色训练工具。
最后,我们学习了如何使用语义文本相似性基准(STSb)评估我们的句子变换器模型——识别表现最佳的模型。
参考文献
[1] N. Reimers, I. Gurevych, Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks (2019), ACL
[2] N. Reimers, Sentence Transformers NLI Training Readme, GitHub
FinOps:降低 BigQuery 存储成本的四种方法
原文:
towardsdatascience.com/finops-four-ways-to-reduce-your-bigquery-storage-cost-82d99c47f139
不要忽视云存储成本
·发表在 Towards Data Science ·9 分钟阅读·2023 年 1 月 30 日
--
图片来源:Nathan Dumlao 在 Unsplash
在当前经济形势下,最大限度地利用现金并制定一系列成本优化策略比以往任何时候都更加重要。云服务的广泛使用不仅带来了许多商业机会,还带来了管理挑战的潜在风险,可能导致成本超支等问题。
FinOps 是一个新引入的概念,是一个不断发展的操作框架和文化转变,它通过云转型将技术、财务和业务结合起来,使组织能够获得最大业务价值。其关键支柱之一是支出控制。它并不是要削减业务,而是更清楚地了解云上的可能性,并优化资源,以更少的支出实现相同的目标。
我们今天关注的领域是 BigQuery 存储成本。许多人认为存储便宜,这并非完全错误。根据云存储和数据备份公司 Backblaze 的数据,自 2009 年以来,每千兆字节的成本已经下降了 90%。
来源:Backblaze
这是否意味着我们在存储上花费的钱会越来越少?不,事实是数据量在过去 10 年里激增了1900%的增长,而且云存储价格在过去五年里停滞不前。由于通货膨胀的影响,许多供应商甚至提高了 2023 年的存储价格。因此,请确保在组织的所有层级中都有一个(接近)实时的计费仪表板,以获得这些洞察。
在这篇文章中,我想介绍四种方法来帮助你的组织降低 BigQuery 存储成本。你会对结果感到惊讶!
我还有一些关于成本优化的文章:BigQuery 的 7 种成本优化实践,我如何构建一个实时 BigQuery 管道以节省成本和进行容量规划。也可以随时查看它们。
BigQuery 存储定价模型
让我们首先看看 BigQuery 存储的定价模型(2023 年 1 月的价格)。BigQuery 提供两种定价模型:逻辑和物理。它是数据集级别的一个属性,称为[storage_billing_model](https://cloud.google.com/bigquery/docs/updating-datasets#update_storage_billing_models)
:
-
逻辑:这是数据集的默认计费模型。数据大小是根据各个列的数据类型来计算的。例如,
INT64
类型占用 8 个逻辑字节。 -
物理:数据大小是根据压缩后存储在磁盘上的数据来计算的。值得注意的是,它包括用于时间旅行存储的字节(默认 7 天)。
BigQuery 存储定价模型(作者创建)
在这两种定价模型中,我们为活跃存储和长期存储支付不同的价格。BigQuery 根据修改自动标记数据为活跃或长期。
-
活跃:在过去 90 天内被修改过的表或表分区。操作包括将数据加载、复制和流式传输到表中,以及使用 DML、DDL 等。仅仅查询表不会使其变为活跃状态。
-
长期:在 90 天内没有被修改的表或表分区。活跃存储和长期存储在性能、耐久性或可用性上没有区别。
将 storage_billing_model 切换到物理
BigQuery 以列式格式存储数据 — Capacitor,这可以实现高达 1:10 的压缩比和高扫描吞吐量。物理模型按压缩字节收费,但它总是比逻辑模型便宜吗?让我们来看几个 BigQuery 公共数据集 的例子。
bigquery-public-data.cloud_storage_geo_index.landsat_index
— 这个表每天都会重新创建,因此每个字节都是活动字节,它没有时间旅行数据。这个表有大约 3GB 的逻辑字节和约 480MB 的物理字节。压缩率接近 86%。相当令人印象深刻!就价格而言,物理模型要便宜得多,因为它没有时间旅行字节,且压缩率超过 50%。
landsat_index
表的存储信息
bigquery-public-data.google_cloud_release_notes.release_note
— 这个表每天更新。总的物理字节数多于逻辑字节数,因为它包括时间旅行数据。由于每个字节都是活动的,切换到物理计费模型实际上会花费更多。
release_note
表的存储信息
bigquery-public-data.crypto_bitcoin.transactions
— 这个表每天都会更新,超过 90%的存储被标记为长期存储。就价格而言,物理模型稍微便宜一些。
transactions
表的存储信息
目前我们不能得出一个计费模型比另一个更具成本效益的结论,因为这取决于数据集中的表是如何被修改的。但这里有一些规则可以帮助你决定:(假设压缩率超过 50%)。
-
如果表没有(或很少有)时间旅行字节,那么选择物理模型。
-
如果表只有活动字节并且使用默认的时间旅行设置,那么考虑保持逻辑模型。
-
如果表中有高比例的长期字节并且使用默认的时间旅行设置,物理模型可能会更便宜,但差别不大。
一个重要的注意事项是,如果你将数据集的存储计费模型更改为使用物理字节,你不能将其更改回使用逻辑字节。因此,切换时请谨慎。
如果你不确定是否切换,可以使用 BigQuery information_schema.table_storage_timeline_by_project
表来每天或每月监控存储元数据,并在趋势更加稳定时进行切换。
使用表克隆/快照而不是表复制
如果你经常需要复制表,这个提示会很有帮助。你可以每天复制表以保持历史记录,或者将表从生产环境复制到测试环境以供测试。有四种方式可以复制表:
-
数据传输服务
-
表复制
-
表克隆
-
表快照
数据传输服务可以在计划管理的基础上自动将数据移动到 BigQuery 中。它可以复制表格和数据集,并将外部数据导入 BigQuery。表格复制是一种表级操作,会创建表格的完整副本。在这两种方法中,复制的表格都是“BASE TABLE”类型。这意味着 BigQuery 将按复制表格的完整存储量收费。
我们可以从
information_schema.tables
中找出表格类型。
表克隆是基表的轻量、可写副本。最棒的是,你只为表克隆中与基表不同的数据存储付费。因此,初始时表克隆没有存储成本!以下图表说明了成本情况。
基表与表克隆之间的存储差异(作者创建)
初始时,当你克隆表格时,新表格没有存储成本,因为它与基表相同。只有当存在差异时才会收费,以下是公式。
New base table: original table + new data - deleted data
[0-9] + [A] + [B] + [C] - [0] - [1]
New table clone: changed data + new data
[0]+[1]+[4]+[7]+[8]+[9]+[D]
[0] + [1] : deleted data in base table which exists in table clone
[4] + [9] : modified data in base table which exists in table clone
[7] + [8] : modified data in table clone which exists in base table
[D]: new data
值得注意的是,对基表的一些更改可能导致你为表克隆支付全部存储费用。例如,如果你修改了带有集群的基表,你将为该基表的任何克隆表中更新的分区支付全部存储费用。在以下图表中,P2 中的一个值更改更新了整个 P2,因此收费整个分区。
基表分区和集群化时的存储差异(作者创建)
拥有分区可以始终帮助降低表克隆的存储成本,因为 BigQuery 仅对修改过的数据分区收费,而不是整个表格。
当你将表复制到测试项目中用于测试目的,并且只更新表格的一部分(例如 20%)时,推荐使用表克隆。在这种情况下,你可以节省原始存储成本的 80%。在最坏的情况下,当你更新整个表格时,表克隆的存储成本将与使用普通表格复制相同。因此,绝对值得一试!
但有一些限制。例如,你不能创建视图、物化视图或外部表的克隆。表克隆必须与基表位于同一地区。此功能目前处于预览阶段(2023 年 1 月),可能支持有限。
表克隆的另一种变体称为表快照。表克隆和表快照之间的区别在于表快照是只读的。它保留了基表在特定时间的内容。对于存储费用,BigQuery 仅对基表中不再存在或已更改的数据收取费用。例如:
基表与表快照之间的存储差异(作者创建)
类似于表克隆,初始存储成本为零。以下是基表更新时的费用。
New table snapshot: deleted from or changed in base table
[0]+[1]+[4]+[9]
[0] + [1] : deleted data in base table which exists in table snapshot
[4] + [9] : modified data in base table which exists in table snapshot
基表中的分区也有助于降低表快照的存储成本。在限制方面,它们类似于表克隆。但表快照是 GA 功能,因此有更多文档支持,并获得 GCP 的更多支持。
如果您希望保留表的历史记录超过 7 天,表快照是有益的。通过 BigQuery 时间旅行,您只能访问 7 天前的表数据。表快照可以根据您的需要保留只读数据。
设置表过期时间
节省成本的一个简单方法是删除未使用的表或表分区。我们经常在测试环境中遇到这样的表。BigQuery 允许我们在分区级、表级和数据集级别设置过期时间。
-- partition level
ALTER TABLE mydataset.mytable SET OPTIONS (partition_expiration_days = 5);
-- table level
ALTER TABLE mydataset.mytable SET OPTIONS (expiration_timestamp = TIMESTAMP '2025-02-03 12:34:56');
-- dataset level
ALTER SCHEMA mydataset SET OPTIONS( default_table_expiration_days = 3.75);
到期后,表或分区将自动删除。请注意,如果您更新数据集的默认表过期时间,它只会适用于新创建的表。
将旧数据保留在 BigQuery 中,而不是导出
BigQuery 不是传统的数据仓库。它已经发展成一个数据湖仓,这是一个结合了数据仓库和数据湖最佳元素的新架构。
我们可以通过查看它们的定价模型来感知这一点。逻辑模型中的活动和长期字节的存储成本与云存储中的标准和近线存储类型相同。然而,云存储有操作费用,如下表所示。
GCP 云存储定价模型(作者创建)
BigQuery 没有这种操作费用,简单查询长期表不会将其更改为活动状态。因此,从成本和操作的角度来看,将旧数据保留在 BigQuery 是一个更可取的选择。
结论
一如既往,希望您觉得这篇文章鼓舞人心且有用。2022 年是运营业务最艰难的一年之一。各种挑战促使工程师从不同的角度审视他们的技术堆栈,从如何扩展系统到如何控制成本以提高业务的韧性。
如果您使用 BigQuery,请与您的同事分享这四个小贴士。我相信它们能对您的业务产生巨大影响,并使公司能够将资金分配到更关键的领域。如果您有任何想法,请在评论中告知我。谢谢!
使用 Apache Spark 进行机器学习的第一步
Spark MLlib 包的基本概念和主题
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 1 月 4 日
--
照片来自 Element5 Digital 在 Unsplash
介绍
Apache Spark 是大数据背景下数据处理和分析的主要工具之一。它是一个非常完整(且复杂)的数据处理框架,其功能大致可以分为四组:SparkSQL & DataFrames,用于通用数据处理需求;Spark Structured Streaming,用于处理数据流;Spark MLlib,专注于机器学习和数据科学;GraphX,图形处理 API。
官方文档中的 Spark 库部分。打印由作者提供。
我已经在其他文章中介绍了前两个:创建一个数据仓库的 ETL 过程 和 将 Spark 和 Kafka 集成进行流处理。今天我们要讨论第三个——让我们使用 Spark MLlib 进行机器学习。
机器学习在我心中占有特殊的位置,因为它是我进入数据科学领域的入口门槛,并且像你们中的许多人一样,我是通过经典的 Scikit-Learn 库开始的。
我本可以写一整篇文章来说明为什么 Scikit-learn 库是如此出色的软件。它对初学者友好,易于使用,涵盖了大部分机器学习周期,文档写得非常好,等等。
但我为什么要谈论这个呢?如果你像我一样,习惯于使用 sklearn 编码,请记住 Apache Spark 的路径并不那么直接。虽然不难,但学习曲线更陡峭。
在这篇文章中,我们将学习如何通过一个动手示例完成数据预处理、特征工程、模型训练和验证的“完整机器学习周期”。
Apache Spark 概述
Apache Spark 是一个基于分布式内存的数据转换引擎。它旨在在分布式环境中运行,通过在机器之间并行处理,利用其惰性计算哲学和查询优化来实现高性能转换。
这就是学习这种工具的主要原因——性能。
即使进行了优化,当数据集变得过大时,Sklearn 包(以及其他 Python 包)也会遇到困难。这是 Spark 能够覆盖的潜在盲点之一。由于 Spark 具有横向扩展的能力,因此更容易增加计算能力来训练大数据上的模型。
问题
我选择了 Avocado Price Dataset 来进行这个项目。我们在这个数据集中的任务是预测给定牛油果类型、日期、可用牛油果袋数及其他特征的平均牛油果价格。有关更多信息,请查看 Kaggle 数据集页面。
环境设置
你只需要安装 docker 和 docker-compose。代码可以在 GitHub 上找到。
文章中描述的架构(在 docker-compose.yaml 文件中)如下面的图像所示。
项目的架构与 Docker 容器。图片由作者提供。
所有代码都在一个 jupyter/pyspark-notebook 容器内开发,所有 pyspark 依赖项都已配置好。
要启动环境,只需运行:
docker-compose up
实现
我们的目标是学习如何使用 Spark 实现我们通常的机器学习流水线,涵盖内容包括:加载数据并将其分为训练集/测试集,清理数据,预处理+特征工程,模型定义,超参数调整和最终评分。
以下章节将详细说明如何完成这些步骤。
连接到 Spark
首先需要连接到 Spark 集群,这一步相当简单。
from pyspark.sql import SparkSession
import pyspark.sql.functions as F
# SparkSession
URL_SPARK = "spark://spark:7077"
spark = (
SparkSession.builder
.appName("spark-ml")
.config("executor.memory", "4g")
.master(URL_SPARK)
.getOrCreate()
)
在第一次运行时可能需要几秒钟。
加载数据
现在是处理数据的时候了。这部分与 Spark 的 MLlib 包无关,仅涉及使用 Spark SQL 加载数据。
df_avocado = spark.read.csv(
"/data/avocado.csv",
header=True,
inferSchema=True
)
# cache data
df_avocado.cache()
df_avocado.show(4)
由于 Spark 是 惰性计算 的,缓存数据集在内存中可以加快后续步骤的执行。
让我们查看一下数据:
关于列的更多细节可以在原始数据集的 Kaggle 页面上找到。
接下来,我们使用 randomSplit() 方法将 DataFrame 拆分为训练集(75%)和测试集(25%)。
df_avocado_train, df_avocado_test = df_avocado.randomSplit([0.75, 0.25], seed=214)
预处理数据
在继续之前,让我们了解一下我们将使用的工具。Spark MLlib 包有两种主要类型的对象:变换器和估算器。
变换器 是能够变换 DataFrame 的对象。它们接收原始 DataFrame 并返回处理后的 DataFrame。常见的变换器包括 PolynomialExpansion、SQLTransformer 和 VectorAssembler(非常重要,后面会讨论)。
估算器,另一方面,是需要在数据上进行拟合/训练以生成变换器的对象。这些包括机器学习预测器(线性回归、逻辑回归、决策树等)、降维算法(PCA、卡方选择器),以及其他列变换器(标准化器、最小最大缩放器、TF-IDF 等)。
Spark MLlib 上的变换器和估算器。作者提供的图片。
我们先用 SQLTransformer 来处理数据。这是一个强大的变换器,允许使用 SQL 查询选择和变换列。
COLUMNS = ['AveragePrice', 'type']
COLUMNS = [f"`{col}`" for col in COLUMNS]
LOG_COLUMNS = ['4225', '4770', 'Small Bags', 'Large Bags', 'XLarge Bags']
LOG_COLUMNS = [f"LOG(`{col}`+1) AS `LOG {col}`" for col in LOG_COLUMNS]
sql_trans = SQLTransformer(
statement=f"""
SELECT
{', '.join(COLUMNS)}
, {', '.join(LOG_COLUMNS)}
,YEAR(__THIS__.Date)-2000 AS year
,MONTH(__THIS__.Date) AS month
FROM __THIS__
"""
)
# Visualize the data
sql_trans.transform(df_avocado_train).show(4)
上面的代码选择了 AveragePrice 和 type 列,使用对数函数转换了数值列,并创建了两个新列,提取 year(2000 年后)和 month。
THIS 是当前正在变换的 DataFrame 的默认名称。
结果:
缩放是在数据预处理中的一种常见做法。让我们使用最小-最大缩放技术缩放 month 列,将所有值放在 [0, 1] 区间内。MinMaxScaler 是一种 估算器,因此需要先在数据上进行拟合,然后才能用来变换数据。
大多数估算器(包括所有预测模型)要求输入列为向量形式。向量是一种在 Spark MLlib 中主要使用的特殊列类型。它正如名字所示,是一个固定大小的数字数组。
为了将列合并成一个单一的向量列,我们使用 VectorAssembler Transformer。
Vector Assembler 正在运行。图片来自作者。
from pyspark.ml.feature import MinMaxScaler
# Creating a Month vector column
month_vec_ass = VectorAssembler(inputCols=['month'], outputCol='month_vec')
df_avocado_month_ass = month_vec_ass.transform(sql_trans.transform(df_avocado_train))
# Scaling the month column
month_scaler = MinMaxScaler(inputCol='month_vec', outputCol='month_scaled')
month_scaler = month_scaler.fit(df_avocado_month_ass)
month_scaler\
.transform(df_avocado_month_ass)\
.select( ['month', 'month_vec', 'month_scaled'] )\
.show(10)
下面的图片详细说明了这个过程。
对列应用 MinMaxScaler 的过程。图片来自作者。
结果:
牢记这些概念,只需了解可用的变换器并在我们的管道中使用它们。
例如,列type有两个值,"conventional"和"organic",需要映射成数字。负责此操作的变换器是 StringIndexer。
它将列中的每个类别分配一个数值。由于“type”列只有两个类别,它将被转换成一个只有两个值的列:0 和 1,这相当于应用了独热编码技术。
str_indexer = StringIndexer(inputCol="type", outputCol="type_index")
str_indexer = str_indexer.fit(df_avocado_train)
str_indexer\
.transform(df_avocado_train)\
.select( ["type", "type_index"] )\
.show(4)
从现在开始,我将总结所做的工作。
生成的数值特征(所有列,除了type_index)被组装成一个名为“features_num”的单一向量,这个最终向量通过一个 StandardScaler。
# Apply transformations
## SQL transformer
df_avocado_train_transformed = sql_trans.transform(df_avocado_train)
## String indexer
df_avocado_train_transformed = str_indexer.transform(df_avocado_train_transformed)
## Month scaler (vector assembler + minmax scaler)
df_avocado_train_transformed = month_vec_ass.transform(df_avocado_train_transformed)
df_avocado_train_transformed = month_scaler.transform(df_avocado_train_transformed)
# Join all features into a single vector
numerical_vec_ass = VectorAssembler(
inputCols=[
'year', 'month_scaled', 'LOG 4225',
'LOG 4770', 'LOG Small Bags',
'LOG Large Bags', 'LOG XLarge Bags'
],
outputCol='features_num'
)
df_avocado_train_transformed = numerical_vec_ass.transform(df_avocado_train_transformed)
# Join all categorical features into a single vector
categorical_vec_ass = VectorAssembler(
inputCols=['type_index'],
outputCol='features_cat'
)
df_avocado_train_transformed = categorical_vec_ass.transform(df_avocado_train_transformed)
# See the result
df_avocado_train_transformed.select(['features_cat', 'features_num', 'AveragePrice']).show(4, False)
# Scaling the numerical features using a StandardScaler
std_scaler = StandardScaler(
inputCol="features_num",
outputCol="features_scaled",
withStd=True,
withMean=True
)
std_scaler = std_scaler.fit(df_avocado_train_transformed)
std_scaler.transform(df_avocado_train_transformed).select(['features_scaled']).show(5, False)
然后将类别列“type_index”添加到最终向量中。
最后一步是将创建的所有变换器合并成一个管道。管道只是一个用于封装一组变换器和估算器的对象,以便将它们顺序应用于数据。这有助于避免单独处理每个中间转换步骤(如我们至今所做的)。
# Machine learning pipeline
from pyspark.ml import Pipeline
# Create a preprocessing pipeline
prepro_pipe = Pipeline(stages=[
sql_trans,
str_indexer,
month_vec_ass,
month_scaler,
numerical_vec_ass,
categorical_vec_ass,
std_scaler,
# Join all features into a single vector
VectorAssembler(
inputCols=['features_scaled', 'features_cat'],
outputCol='features'
),
])
# Fit the pipeline
pipeline_model = prepro_pipe.fit(df_avocado_train)
# Transform the data
df_avocado_train_transformed = pipeline_model.transform(df_avocado_train)
# See the result
df_avocado_train_transformed.select(['features', 'AveragePrice']).show(4, False)
最终结果:
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+
|features |AveragePrice|
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+
|[-1.2177154955881637,1.6482225355667333,0.9527463109714546,1.0269649008115518,0.5657377199959452,0.8334134211814762,-0.6436162273445295,0.0]|0.49 |
|[-1.2177154955881637,1.6482225355667333,0.7058305701685025,1.0954357394643428,0.7803295242390127,0.8574417380503548,2.012648481596976,0.0] |0.71 |
|[-1.2177154955881637,1.6482225355667333,0.9399552148956506,1.5037797059140563,0.8203168521795554,0.6002078289352569,2.1083545825302594,0.0] |0.8 |
|[-1.2177154955881637,1.6482225355667333,1.1142436751287843,1.5073956355774096,1.4653967110976907,1.0678725104034048,2.0181300922626053,0.0] |0.8 |
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+
only showing top 4 rows
模型训练
这就是我们都在等待的时刻。
在经过漫长的数据预处理路径之后,所有特征已经以其所需的最终向量形式存在,我们准备好训练模型了。
不幸的是,这部分将比前一部分短很多 ¯_(ツ)_/¯
如前所述,ML 模型只是估算器,因此过程会重复:实例化、拟合和转换。
让我们训练一个线性回归模型:
from pyspark.ml.regression import LinearRegression
# Create a linear regression model
lin_reg = LinearRegression(
featuresCol='features',
labelCol='AveragePrice',
predictionCol='prediction',
# Hyperaparameters
maxIter=1000,
regParam=0.3, # Regularization
elasticNetParam=0.8 # Regularization mixing parameter. 1 for L1, 0 for L2.
)
必须指定特征列、目标/标签列以及预测列的名称。就像我们遇到的其他估算器一样,ML 模型只会向 DataFrame 中添加另一列。
机器学习模型只是一个估算器。图片来自作者。
# Fit the model
lin_reg_model = lin_reg.fit(df_avocado_train_transformed)
# See the output
df_avocado_train_pred = lin_reg_model.transform(df_avocado_train_transformed)
df_avocado_train_pred.select(
['features', 'AveragePrice', 'prediction']
).show(4, False)
见下图结果:
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+------------------+
|features |AveragePrice|prediction |
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+------------------+
|[-1.2177154955881637,1.6482225355667333,0.9527463109714546,1.0269649008115518,0.5657377199959452,0.8334134211814762,-0.6436162273445295,0.0]|0.49 |1.4003505112793717|
|[-1.2177154955881637,1.6482225355667333,0.7058305701685025,1.0954357394643428,0.7803295242390127,0.8574417380503548,2.012648481596976,0.0] |0.71 |1.4003505112793717|
|[-1.2177154955881637,1.6482225355667333,0.9399552148956506,1.5037797059140563,0.8203168521795554,0.6002078289352569,2.1083545825302594,0.0] |0.8 |1.4003505112793717|
|[-1.2177154955881637,1.6482225355667333,1.1142436751287843,1.5073956355774096,1.4653967110976907,1.0678725104034048,2.0181300922626053,0.0] |0.8 |1.4003505112793717|
+--------------------------------------------------------------------------------------------------------------------------------------------+------------+------------------+
only showing top 4 rows
模型评估
要评估模型的性能,我们需要一个评估器。我认为它的名字已经很直观了,它将计算真实标签与模型预测之间的性能指标。
评估器在操作中。图像来源:作者。
在下面的单元格中,实例化一个 RegressionEvaluator 来测量预测值与实际值(在训练数据上)之间的 RMSE(均方根误差)。
from pyspark.ml.evaluation import RegressionEvaluator
reg_eval = RegressionEvaluator(
labelCol='AveragePrice',
predictionCol='prediction',
metricName='rmse' # Root mean squared error
)
# Evaluate the model
reg_eval.evaluate(df_avocado_train_pred)
# Output >> 0.3978489578943717
使用交叉验证进行超参数调优
超参数调优是机器学习管道中的最后阶段之一,因此我们的冒险即将结束。
在这一步,我们测试模型/管道的多个超参数变体,以根据选择的指标挑选最佳的一个。常用的方法是使用交叉验证技术。
在这里,我们遇到了今天文章中的最后构建块——ParamGridBuilder和CrossValidator。
从 ParamGridBuilder 开始:这是用来构建超参数网格的对象。
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
ml_pipeline = Pipeline(stages=[
prepro_pipe, # Preprocessing pipeline
lin_reg # Linear regression model
])
param_grid = ParamGridBuilder() \
.addGrid(lin_reg.regParam, [0.0, 0.1, 0.3, 0.5]) \
.addGrid(lin_reg.elasticNetParam, [0.0, 0.5, 1.0]) \
.build()
在上面的代码中,为线性回归的regParam和elasticNetParam指定了几个值。重要的是要注意,原始对象用于引用这些参数。
然后,CrossValidator将所有内容(估计器、超参数网格和评估器)结合起来……
reg_eval = RegressionEvaluator(
labelCol='AveragePrice',
predictionCol='prediction',
metricName='rmse' # Root mean squared error
)
# Join everything together using a CrossValidator object.
crossval_ml = CrossValidator(
estimator=ml_pipeline,
estimatorParamMaps=param_grid,
evaluator=reg_eval,
numFolds=4
)
… 并在调用方法fit()时,对给定数量的折数进行交叉验证。
crossval_ml_model = crossval_ml.fit(df_avocado_train)
结果通过拟合的交叉验证对象来访问。下面的代码打印出最佳模型的名称和得分。
best_model = crossval_ml_model.bestModel
best_score = crossval_ml_model.avgMetrics[0]
print("Best model: ", best_model)
print("Best score: ", best_score)
# Output >>>
# > Best model: PipelineModel_dc90de555ac1
# > Best score: 0.2833541578138277
让我们还查看一下线性回归的最佳参数。
# The last stage in the pipeline is the Linear Regression
best_lin_reg_params = best_model.stages[-1].extractParamMap()
print("Best score (RMSE):", best_score, end="\n\n")
for parameter, value in best_lin_reg_params.items():
print(f"{str(parameter):50s}, {value}")
输出:
Best score (RMSE): 0.2833541578138277
LinearRegression_eeaa1d8bf6ea__aggregationDepth , 2
LinearRegression_eeaa1d8bf6ea__elasticNetParam , 0.0
LinearRegression_eeaa1d8bf6ea__epsilon , 1.35
LinearRegression_eeaa1d8bf6ea__featuresCol , features
LinearRegression_eeaa1d8bf6ea__fitIntercept , True
LinearRegression_eeaa1d8bf6ea__labelCol , AveragePrice
LinearRegression_eeaa1d8bf6ea__loss , squaredError
LinearRegression_eeaa1d8bf6ea__maxBlockSizeInMB , 0.0
LinearRegression_eeaa1d8bf6ea__maxIter , 1000
LinearRegression_eeaa1d8bf6ea__predictionCol , prediction
LinearRegression_eeaa1d8bf6ea__regParam , 0.0
LinearRegression_eeaa1d8bf6ea__solver , auto
LinearRegression_eeaa1d8bf6ea__standardization , True
LinearRegression_eeaa1d8bf6ea__tol , 1e-06
在测试集上评估最佳模型
所有的一切都汇聚到这一刻。这是我们在测试数据上衡量最佳模型性能的步骤。
幸运的是,这里没有新的内容需要学习,这只是将最佳模型应用于测试数据,并将结果传递给评估器的问题。
df_avocado_test_pred = best_model.transform(df_avocado_test)
# show scores
print(reg_eval.evaluate(df_avocado_test_pred))
# Output
# > 0.28368085199676235
性能与交叉验证步骤中获得的性能非常相似。
结论
随着 ML 应用的普及和其要求的复杂化,掌握各种用途不同的工具知识变得至关重要。
在这篇文章中,我们了解了如何通过 Spark MLlib 模块在机器学习的背景下使用 Apache Spark。通过一个实际项目,我们创建了一个通用的 ML 管道,涵盖了该模块的主要概念和基本主题。
学习一种新工具主要涉及熟悉其词汇,即,理解组成它的基本部分以及如何利用它们来解决问题。因此,我们专注于理解 Spark MLlib 的基础知识:估计器、转换器、评估器和管道。
我希望这篇简短的文章能帮助你理解 Spark 如何在机器学习应用中使用。
一如既往,这篇文章只是略微触及了所探讨主题的表面,因此我强烈建议进一步阅读,见下文的参考文献。
感谢阅读 😉
参考文献
所有代码都可以在这个GitHub 仓库中找到。
使用的数据 — 鳄梨价格, ODbL v1.0:开放数据库, Kaggle.
[1] Chambers, B., & Zaharia, M. (2018). Spark: 终极指南:简化的大数据处理。“ O’Reilly Media, Inc.”
[2] Spark 示例 — sparkbyexamples.com/
[3] Géron, A. (2022). 动手实践机器学习:使用 Scikit-Learn、Keras 和 TensorFlow。“ O’Reilly Media, Inc.”.
[4] 概述:估算器、转换器和管道 — spark.ml。Spark 官方文档.
使用 Python 探索强化学习的第一步
原文:
towardsdatascience.com/first-steps-in-the-world-of-reinforcement-learning-using-python-b843b76538e3
原始的 Python 实现,展示了如何在强化学习的基本世界之一——网格世界中找到最佳位置
·发表于 Towards Data Science ·15 分钟阅读·2023 年 1 月 13 日
--
网格世界矩阵;作者提供的照片
本文的目的是使用 Python 代码和注释介绍强化学习(以下简称RL)中的基本概念和定义。
这篇文章深受以下伟大强化学习课程的启发:www.coursera.org/learn/fundamentals-of-reinforcement-learning
理论见书¹:www.incompleteideas.net/book/RLbook2020.pdf
我所有的强化学习实验代码可以在我的 Gitlab 仓库中查看:github.com/Eligijus112/rl-snake-game
网格世界问题是强化学习中的一个经典问题,我们希望为代理创建一个优化的策略以穿越网格。
网格是一个方形的单元格矩阵,代理可以在每个单元格中向任意四个方向(上、下、左、右)移动。代理每移动一步会获得-1 的奖励,若达到目标单元格则获得+10 的奖励。奖励的数值是任意的,可以由用户定义。
在强化学习框架中,代理被正式定义为做出采取何种行动的决策的组件。代理在具体的时间步骤中采取行动。
在网格世界设置中,整个动作集合由以下集合定义:
动作集合
或
动作集合
无论我们的代理处于何处,它只能向左、向右、向上或向下移动。现在让我们定义并可视化我们的网格世界:
def array_index_to_matplot_coords(i: int, j: int, n_cols: int) -> Tuple[int, int]:
"""Converts an array index to a matplot coordinate"""
x = j
y = n_cols - i - 1
return x, y
def plot_matrix(
M: np.array,
goal_coords: list = [],
img_width: int = 5,
img_height: int = 5,
title: str = None,
annotate_goal: bool = True
) -> None:
"""
Plots a matrix as an image.
"""
height, width = M.shape
fig = plt.figure(figsize=(img_width, img_width))
ax = fig.add_subplot(111, aspect='equal')
for y in range(height):
for x in range(width):
# By default, the (0, 0) coordinate in matplotlib is the bottom left corner,
# so we need to invert the y coordinate to plot the matrix correctly
matplot_x, matplot_y = array_index_to_matplot_coords(x, y, height)
# If there is a tuple of (x, y) in the goal_coords list, we color the cell gray
if (x, y) in goal_coords:
ax.add_patch(matplotlib.patches.Rectangle((matplot_x - 0.5, matplot_y - 0.5), 1, 1, facecolor='gray'))
if annotate_goal:
ax.annotate(str(M[x][y]), xy=(matplot_x, matplot_y), ha='center', va='center')
else:
ax.annotate(str(M[x][y]), xy=(matplot_x, matplot_y), ha='center', va='center')
offset = .5
ax.set_xlim(-offset, width - offset)
ax.set_ylim(-offset, height - offset)
ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)
plt.title(title)
plt.show()
# Importing the array library
import numpy as np
# Defining the number of blocks of a n x n grid
n = 7
# Defining the value for the hole and the goal
goal = 10
step = -1
# Initiating an empty dataframe of size n x n
R = np.ones((n,n))
# Defining the coordinates of the goal
goal_coords = [(0, n-1), (n-1, 0), (0, 0), (n-1, n-1), (n // 2, n // 2)]
# Adding the goal values to the center and the corners
for goal_coord in goal_coords:
R[goal_coord[1], goal_coord[0]] = goal
# Every other step is -1
R[R == 1] = step
# Converting the G matrix to int
R = R.astype(int)
# Ploting
plot_matrix(R, goal_cords, title='Gridworld')
网格世界;作者照片
在上述示例中,我们定义了第一个所需的矩阵——R矩阵或奖励矩阵。目标位于网格世界的中心和角落。当代理进入其中一个单元格时,它会获得该单元格的奖励值。
让我们定义另一个关键矩阵——状态矩阵S:
S = np.arange(0, n*n).reshape(n, n)
plot_matrix(S, goal_coords, title='State space')
状态空间;作者照片
在我们定义的网格世界中,总共有49 个状态,代理可以处于其中的任何一个状态。每个状态可以通过矩阵中的整数来识别。
假设我们的代理在状态 17 并向下移动。该动作值,表示为 q,是:
状态 17 中动作‘down’的动作值
动作值为 10,因为网格 24 的奖励等于 10。因此,当我们使用动作值时,需要注意网格S索引和奖励矩阵G。可以很容易猜到,从同一状态向右移动的奖励是-1:
状态 17 中动作‘right’的动作值
一般来说,函数 q,称为动作值,将一个数字映射到状态—动作对:
动作值函数
数字越高,对代理的“奖励”就越大,因此,代理总是希望采取最大化****当前状态下的 q的动作。
到目前为止,我们已经定义了矩阵R(奖励)和S(状态)。另一个关键矩阵是状态值矩阵V。V矩阵的维度与 S 和 G 矩阵相同,每个V矩阵中的元素评估给定状态的“好坏”。这里的“好坏”指的是方程:
状态 s 的值[1]
我们可以将上述方程读作:
给定策略 pi 的状态 s 的值等于在时间步 t 给定状态为 s 时的期望回报。
我们计算所有状态的上述值并将其存储在矩阵V中。我们在这里引入了新的变量,所以让我们定义它们。
时间步 t 的总回报[1]
折扣因子[1]
K索引称为终端状态,其中代理达到网格世界中的任意目标。换句话说,每个状态中 G 的值表示从给定状态开始朝向目标的代理路径的折扣奖励总和。值越大,状态越受欢迎。
状态方程中的 pi 项称为策略,是采取状态 s 中某一动作的概率:
策略
让我们初始化初始值矩阵V:
# Initiating the empty Value function
V = np.zeros((n, n))
plot_matrix(V, goal_coords, title='Value function')
初始状态值矩阵;作者照片
由于我们尚未探索我们创建的网格世界,因此所有状态的回报都是 0。
我们需要的最后一个矩阵是策略矩阵P。
def plot_policy_matrix(P: dict, S:np.array, goal_coords: list = [], img_width: int = 5, img_height: int = 5, title: str = None) -> None:
"""
Plots the policy matrix out of the dictionary provided; The dictionary values are used to draw the arrows
"""
height, width = S.shape
fig = plt.figure(figsize=(img_width, img_width))
ax = fig.add_subplot(111, aspect='equal')
for y in range(height):
for x in range(width):
matplot_x, matplot_y = array_index_to_matplot_coords(x, y, height)
# If there is a tuple of (x, y) in the goal_coords list, we color the cell gray
if (x, y) in goal_coords:
ax.add_patch(matplotlib.patches.Rectangle((matplot_x - 0.5, matplot_y - 0.5), 1, 1, facecolor='gray'))
else:
# Adding the arrows to the plot
if 'up' in P[S[x, y]]:
plt.arrow(matplot_x, matplot_y, 0, 0.3, head_width = 0.05, head_length = 0.05)
if 'down' in P[S[x, y]]:
plt.arrow(matplot_x, matplot_y, 0, -0.3, head_width = 0.05, head_length = 0.05)
if 'left' in P[S[x, y]]:
plt.arrow(matplot_x, matplot_y, -0.3, 0, head_width = 0.05, head_length = 0.05)
if 'right' in P[S[x, y]]:
plt.arrow(matplot_x, matplot_y, 0.3, 0, head_width = 0.05, head_length = 0.05)
offset = .5
ax.set_xlim(-offset, width - offset)
ax.set_ylim(-offset, height - offset)
ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)
plt.title(title)
# Saving all the unique states to a vector
states = np.unique(S)
# Dictionary to hold each action for a given state
P = {}
for s in states:
s_dict = {}
# Checking which index is the current state in the S matrix
s_index = np.where(S == s)
# If the state is in the top left corner, we can only move right and down
if s_index == (0, 0):
s_dict['right'] = 0.5
s_dict['down'] = 0.5
# If the state is in the top right corner, we can only move left and down
elif s_index == (0, n - 1):
s_dict['left'] = 0.5
s_dict['down'] = 0.5
# If the state is in the bottom left corner, we can only move right and up
elif s_index == (n - 1, 0):
s_dict['right'] = 0.5
s_dict['up'] = 0.5
# If the state is in the bottom right corner, we can only move left and up
elif s_index == (n - 1, n - 1):
s_dict['left'] = 0.5
s_dict['up'] = 0.5
# If the state is in the first row, we can only move left, right, and down
elif s_index[0] == 0:
s_dict['left'] = 0.333
s_dict['right'] = 0.333
s_dict['down'] = 0.333
# If the state is in the last row, we can only move left, right, and up
elif s_index[0] == n - 1:
s_dict['left'] = 0.333
s_dict['right'] = 0.333
s_dict['up'] = 0.333
# If the state is in the first column, we can only move up, down, and right
elif s_index[1] == 0:
s_dict['up'] = 0.333
s_dict['down'] = 0.333
s_dict['right'] = 0.333
# If the state is in the last column, we can only move up, down, and left
elif s_index[1] == n - 1:
s_dict['up'] = 0.333
s_dict['down'] = 0.333
s_dict['left'] = 0.333
# If the state is in the middle, we can move in all directions
else:
s_dict['up'] = 0.25
s_dict['down'] = 0.25
s_dict['left'] = 0.25
s_dict['right'] = 0.25
# Saving the current states trasition probabilities
P[s] = s_dict
# Drawing a plot for the policy matrix with arrows; In one cell there can be the maximum of 4 arrows each indicating the action an agent can take
plot_policy_matrix(P, S, goal_coords, title='Policy matrix')
初始策略矩阵;作者照片
网格中的每个箭头代表代理可以采取的可用动作。初始矩阵中的概率是均匀的,目标状态中没有可用的移动。
拥有R, P, S和V矩阵后,我们可以最终开始计算我们 RL 问题的答案。但我们还需定义 RL 目标。
RL 算法的目标是使代理找到最优策略 P,以最大化每个状态的回报。
另一种表述是目标是计算矩阵 V 中的最优状态值。
假设我们有一个 5 乘 5 的网格,目标在中央:
示例网格世界;作者照片
为了建立直观理解,我已经计算了最佳值和策略。我们将在本文章的下一部分找到如何实现,但现在让我们解释以下V和P矩阵:
已解决的值和策略矩阵;作者照片
记住,V 矩阵中的每个值是总的累计折扣奖励。因此,我们的代理将始终希望前往具有最高值的状态。用数学表达就是,在每个状态下,代理将根据以下方程选择下一个状态:
每个状态的最佳选择 [1]
在每个状态下,我们将选择一个动作,使我们进入状态s prime,在这里r + gamma * (new state value)是最高的。
更简单的直观理解是,我们可以列出当前状态下所有可用的动作,检查哪个可用状态具有最高的V(s)值,然后前往那里。从上面的矩阵中,我们可以看到,例如,状态 8 有两个最佳选择——下和左。这是因为这些动作将使代理进入同样好的状态。因此,拥有V矩阵后,我们将始终推断出 P 矩阵。
现在,如何从全零的 V 矩阵转换为具有值的矩阵?我们需要为每个状态定义贝尔曼方程:
状态 s 的贝尔曼方程与策略 pi [1]
上面的方程令人望而生畏,且具有递归特性。对于网格世界示例,我们可以简化方程,并将其写出,而无需中间的条件概率。
简化方程 [1]
我们可以这样做,因为当我们在状态 s 中执行一个动作时,我们可以保证只会进入一个下一个状态。
在已解决的矩阵示例中,回顾一下
状态 0 的值
这意味着从位置 0 开始,我们的代理在长期内将累计 -6.07 的总奖励。为了估计这一点并将递归公式转换为可以通过简单循环评估的公式,我们将使用以下算法:
价值迭代算法 [1]
我们将简化网格世界问题算法的中间部分:
价值迭代算法简化;作者拍摄
现在让我们将一切转到 Python 代码中。
def get_next_state(a: str, s: int, S: np.array):
"""
Function that returns the next state's coordinates given an action and a state
"""
# Getting the current indexes
s_index = np.where(S == s)
s_row = s_index[0][0]
s_col = s_index[1][0]
# Defining the indexes of the next state
next_row = s_row
next_col = s_col
if a == 'up':
next_row = s_row - 1
next_col = s_col
elif a == 'down':
next_row = s_row + 1
next_col = s_col
elif a == 'left':
next_row = s_row
next_col = s_col - 1
elif a == 'right':
next_row = s_row
next_col = s_col + 1
return next_row, next_col
def bellman_value(
s: int,
S: np.array,
P: dict,
G: np.array,
V: np.array,
gamma: float = 0.9
) -> Tuple:
"""
Calculates the Belman equation value for the given state
"""
# Extracting all the available actions for the given state
actions = P[s]
# Placeholder to hold the sum
sum = 0
for action in actions:
# Extracting the probability of the given action
prob = actions[action]
# Getting the next states indexes
next_row, next_col = get_next_state(action, s, S)
# Extracting the expected reward
reward = G[next_row, next_col]
# Extracting the value of the next state
value_prime = V[next_row, next_col]
# Adding to the sum
sum += prob * (reward + gamma * value_prime)
return sum
上述函数找到状态 s 的贝尔曼方程值。
def get_max_return(s: int, S: np.array, P: dict, G: np.array, V: np.array, gamma: float = 0.9) -> Tuple:
"""
Returns the best action and the Bellman's value for the given state
"""
# Extracting all the available actions for the given state
actions = P[s]
# Placeholder to hold the best action and the max return
best_action = None
max_return = -np.inf
for action in actions:
# Getting the probability of the action
prob = actions[action]
# Getting the next states indexes
next_row, next_col = get_next_state(action, s, S)
# Extracting the expected reward
reward = G[next_row, next_col]
# Extracting the value of the next state
value_prime = V[next_row, next_col]
# Calculating the return
_return = prob * (reward + gamma * value_prime)
# Checking if the return is greater than the current max return
if _return > max_return:
best_action = action
max_return = _return
return best_action, max_return
def update_value(s, S, P, G, V, gamma) -> float:
"""
Updates the value function for the given state
"""
# Getting the indexes of s in S
s_index = np.where(S == s)
s_row = s_index[0][0]
s_col = s_index[1][0]
# Getting the best action and the Bellman's value
_, max_return = get_max_return(s, S, P, G, V, gamma)
# Rounding up the bellman value
max_return = np.round(max_return, 2)
# Updating the value function with a rounded value
V[s_row, s_col] = max_return
return max_return
def value_iteration(
S: np.array,
P: np.array,
G: np.array,
V: np.array,
gamma: float = 0.9,
epsilon: float = 0.0001,
n_iter: int = None
) -> None:
"""
Function that performs the value iteration algorithm
The function updates the V matrix inplace
"""
# Iteration tracker
iteration = 0
# Iterating until the difference between the value functions is less than epsilon
iterate = True
while iterate:
# Placeholder for the maximum difference between the value functions
delta = 0
# Updating the iteration tracker
iteration += 1
# Iterating over the states
for s in S.flatten():
# Getting the indexes of s in S
s_index = np.where(S == s)
s_row = s_index[0][0]
s_col = s_index[1][0]
# Saving the current value for the state
v_init = V[s_row, s_col].copy()
# Updating the value function
v_new = update_value(s, S, P, G, V, gamma)
# Updating the delta
delta = np.max([delta, np.abs(v_new - v_init)])
if (delta < epsilon) and (n_iter is None):
iterate = False
break
if (n_iter is not None) and (iteration >= n_iter):
iterate = False
# Printing the iteration tracker
print(f"Converged in {iteration} iterations")
return None
上述代码块实现了价值迭代算法,用于寻找最佳(或接近最佳的) V 矩阵。
def update_policy(S, P, V):
"""
Function that updates the policy given the value function
"""
# Iterating over the states
for s in S.flatten():
# Listing all the actions
actions = P[s]
# For each available action, getting the Bellman's value
values = {}
for action in actions.keys():
# Getting the next state indexes
next_row, next_col = get_next_state(action, s, S)
# Saving the value function of that nex t state
values[action] = V[next_row, next_col]
# Extracting the maximum key value of the values dictionary
max_value = max(values.values())
# Leaving the keys that are equal to the maximum value
best_actions = [key for key in values if values[key] == max_value]
# Getting the length of the dictionary
length = len(values)
# Creating the final dictionary with all the best actions in it
p_star = {}
for action in best_actions:
p_star[action] = 1/length
# Updating the policy
P[s] = p_star
我们现在有了所有理论和代码,开始评估网格世界中的所有状态。回顾一下,我们的初始网格世界如下:
状态空间;作者拍摄
网格世界;作者拍摄
初始值和策略矩阵;作者拍摄
现在让我们更新一个状态——第一个或 s = 1。
update_value(1, S, P, G, V, gamma=0.9)
update_policy(S, P, V)
价值矩阵和策略矩阵现在看起来如下:
一个状态的价值迭代;作者拍摄
在状态 2 或 8 中,最佳策略是移动到状态 1,因为 0 < 2.66,因此状态 1 比其邻居更有价值。
现在让我们更新状态 3,看看会发生什么:
update_value(3, S, P, G, V, gamma=0.9)
update_policy(S, P, V)
更新第三状态;作者拍摄
在状态 3 中的值为 -1,因此,目前在我们的网格世界中,代理会倾向于避免这个状态,相比于其邻居。
价值迭代算法以与上述相同的方式工作,只是针对所有状态(在我们的案例中——从状态 0 到 48)。要实现它,请使用以下代码:
value_iteration(S, P, G, V, epsilon=10**-16)
update_policy(S, P, V)
已解决的网格世界;作者拍摄
代理可以从任何非终结状态开始,并沿着策略矩阵中的箭头移动。如果同一状态中有两个或更多箭头,我们可以以相同的概率移动到箭头指向的每一个状态。
总结一下,在一个简单的强化学习问题中,我们有 4 个主要矩阵:
-
奖励矩阵 R
-
状态值函数 V
-
策略矩阵 P
-
状态矩阵 S。
此外,我们需要一个有限的动作集合 A。
为了理论上评估每个状态,我们使用贝尔曼方程:
状态 s 的贝尔曼方程与策略 pi
为了实际评估状态值,我们使用价值迭代算法:
价值迭代算法简化;作者拍摄
强化学习任务的目标是找到我们的代理可以遵循的最佳策略。
随意使用代码并在此处进行调整:github.com/Eligijus112/rl-snake-game
。
祝学习愉快!
[1]
作者:理查德·S·萨顿,安德鲁·G·巴托
年份:2018
标题:强化学习:导论
链接:http://archive.ics.uci.edu/ml
FitBot — 一款健身聊天机器人代理
如何创建一个利用 OpenAI 函数调用的聊天机器人代理
·
关注 发布于 数据科学前沿 ·13 分钟阅读·2023 年 7 月 28 日
--
照片由 Gary Butterfield 提供,刊登于 Unsplash
介绍
在健康意识居于前沿的时代,追求平衡生活已成为普遍的愿望,营养无疑是核心支柱。
然而,饮食计划的复杂性和大量的营养数据常常成为我们实现这种平衡的障碍。一个常见的情况是糖尿病患者,他们需要持续和准确的营养指导以有效管理血糖水平。拥有一个个性化的营养助手会不会是一次变革性的体验?
在这种情况下,利用技术来辅助营养指导不仅是有益的,而且是必要的。通过将尖端的人工智能(AI)与全面的营养数据库整合,能够创建一个强大的工具,帮助个人在健康旅程中取得进展。
该项目的代码在这个 GitHub 仓库中: 链接**
项目概述
项目的核心涉及构建一个名为 FitBot 的聊天机器人,该机器人由 OpenAI 的功能驱动,并基于 ReAct(推理和行动)框架(见图 1)。
它旨在提供营养信息和建议,通过解读用户的饮食习惯并整合营养数据的 API 来实现这一目标。
技术方法
通过利用 ReAct 框架,FitBot 保持了对话的互动性质,并能对每条建议提供详细的解释。它还连接到外部营养数据库,确保提供准确且最新的饮食建议。
图 1:ReAct 结合了推理(例如,链式思维提示)和行动。图片来源:链接
FitBot 的幕后
FitBot 将 OpenAI 的 GPT-4 能力与 ReAct 的动态处理结合起来,以理解饮食查询,建议合适的替代方案,并提供个性化建议。它在保持对话语气的同时,使营养建议变得易于获取和引人入胜。
FitBot 的独特之处在于其能够与外部营养数据库接口。这使得 FitBot 能够向用户提供准确和最新的信息,确保所提供的建议是可靠的,并基于准确的数据。
在接下来的部分,我们将深入探讨代码,看看 FitBot 的组件是如何开发和相互作用的,全面了解这个创新项目的内部工作。
构建 FitBot:代码解析
这个项目有四个主要脚本,用于处理数据并在用户界面中显示数据:
-
fitness_agent.py:此文件包含
FitnessAgent
类,该类利用 OpenAI 功能实现 FitBot 所需的功能。 -
chatbot.py:此文件包含 FitBot 用户界面的代码,使用 Gradio 库实现。
-
agents.py:这个文件包含
Agent
类,用于处理与 OpenAI API 的对话。这段代码基于由James Briggs在 funkagent 库中开发的this script。 -
parser.py:这个文件包含将函数文档字符串解析为 OpenAI 函数描述的代码。
定义我们的功能
创建能够提供准确和有用的营养和健身建议的聊天机器人时,我们需要考虑什么信息对最终用户最有价值。这就是前面解释的功能实现背后的原因。
1.**get_nutritional_info**
:这个功能对于任何以健身为导向的聊天机器人都至关重要。人们经常缺乏关于他们所吃食物的营养含量的清晰信息。通过使用Nutrition endpoint from API Ninjas来获取各种食物实时营养数据,我们可以帮助用户做出明智的饮食决策。返回的数据可以包括卡路里、蛋白质、碳水化合物、脂肪等详细信息,全面了解食物项的营养概况。
def get_nutritional_info(self, query: str) -> dict:
"""Fetches the nutritional information for a specific food item
:param query: The food item to get nutritional info for
:return: The nutritional information of the food item
"""
api_url = 'https://api.api-ninjas.com/v1/nutrition?query={}'.format(query)
response = requests.get(api_url, headers={'X-Api-Key': self.nut_api_key})
if response.status_code == requests.codes.ok:
return response.json() # Use json instead of text for a more structured data
else:
return {"Error": response.status_code, "Message": response.text}
2.**calculate_bmr**
:基础代谢率(BMR)是理解个体新陈代谢的关键指标。它是在休息时消耗的能量量,与一个人的年龄、体重、身高和性别密切相关。计算 BMR 的能力为聊天机器人提供了一个基准,帮助用户理解他们的身体即使没有任何身体活动也需要多少卡路里。
def calculate_bmr(weight: float, height: float, age: int, gender: str, equation: str = 'mifflin_st_jeor') -> float:
"""Calculates the Basal Metabolic Rate (BMR) for a person
:param weight: The weight of the person in kg
:param height: The height of the person in cm
:param age: The age of the person in years
:param gender: The gender of the person ('male' or 'female')
:param equation: The equation to use for BMR calculation ('harris_benedict' or 'mifflin_st_jeor')
:return: The BMR of the person
"""
if equation.lower() == 'mifflin_st_jeor':
if gender.lower() == 'male':
return (10 * weight) + (6.25 * height) - (5 * age) + 5
else: # 'female'
return (10 * weight) + (6.25 * height) - (5 * age) - 161
else: # 'harris_benedict'
if gender.lower() == 'male':
return 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age)
else: # 'female'
return 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age)
3.**calculate_tdee**
:了解一个人的总日能量消耗(TDEE)对于制定个性化的饮食或锻炼计划至关重要。TDEE 不仅考虑了 BMR,还考虑了日常活动和锻炼中消耗的卡路里。了解他们的 TDEE 可以帮助用户更有效地计划他们的饮食和锻炼计划,以维持、减少或增加体重。
def calculate_tdee(bmr: float, activity_level: str) -> float:
"""Calculates the Total Daily Energy Expenditure (TDEE) for a person
:param bmr: The BMR of the person
:param activity_level: The activity level of the person
('sedentary', 'lightly_active', 'moderately_active', 'very_active', 'super_active')
:return: The TDEE of the person
"""
activity_factors = {
'sedentary': 1.2,
'lightly_active': 1.375,
'moderately_active': 1.55,
'very_active': 1.725,
'super_active': 1.9,
}
return bmr * activity_factors.get(activity_level, 1)
4.**calculate_ibw**
:了解理想体重(IBW)可以为用户提供一个被认为对他们的身高和性别健康的体重目标。虽然 IBW 不是一个完美的衡量标准(它没有考虑肌肉质量等因素),但它确实为用户提供了一个关于他们的体重应该达到的大致想法,以达到最佳健康。
def calculate_ibw(height: float, gender: str) -> float:
"""Calculates the Ideal Body Weight (IBW)
:param height: The height of the person in inches
:param gender: The gender of the person ("male" or "female")
:return: The Ideal Body Weight in kg
"""
if gender.lower() == 'male':
if height <= 60: # 5 feet = 60 inches
return 50
else:
return 50 + 2.3 * (height - 60)
elif gender.lower() == 'female':
if height <= 60:
return 45.5
else:
return 45.5 + 2.3 * (height - 60)
else:
raise ValueError("Invalid gender. Expected 'male' or 'female'.")
4. **calculate_bmi**
:身体质量指数(BMI)是使用一个人的身高和体重进行简单计算的。公式是 BMI = kg/m²,其中 kg 是一个人的体重(单位:千克),m² 是他们的身高的平方(单位:米)。BMI 并不直接测量体脂肪,但研究表明 BMI 与更直接的体脂肪测量指标有中等相关性。它提供了一个有用的标准来理解一个人是偏瘦、健康、超重还是肥胖。
def calculate_bmi(weight: float, height: float) -> float:
"""Calculates the Body Mass Index (BMI) for a person
:param weight: The weight of the person in kg
:param height: The height of the person in cm
:return: The BMI of the person
"""
height_meters = height / 100 # convert cm to meters
bmi = weight / (height_meters ** 2)
return round(bmi, 2) # round to 2 decimal places for readability
构建代理:包装功能
在建立了必要的功能后,我们的下一步是将这些功能集成到我们的聊天机器人代理中。这种封装使得机器人能够利用这些功能,并根据用户查询提供相关、准确的回应。
以下是如何创建代理的方法:
# Instantiate the agent
fitness_agent = FitnessAgent(openai_api_key, nut_api_key)
你可以查看它所增强的功能:
# You can view the processed function instructions
print(fitness_agent.functions)
输出:
[
{
"name":"get_nutritional_info",
"description":"Fetches the nutritional information for a specific food item",
"parameters":{
"type":"object",
"properties":{
"query":{
"description":"The food item to get nutritional info for",
"type":"string"
}
}
},
"required":[
"query"
]
},
{
"name":"calculate_bmr",
"description":"Calculates the Basal Metabolic Rate (BMR) for a person",
"parameters":{
"type":"object",
"properties":{
"weight":{
"description":"The weight of the person in kg",
"type":"number"
},
"height":{
"description":"The height of the person in cm",
"type":"number"
},
"age":{
"description":"The age of the person in years",
"type":"integer"
},
"gender":{
"description":"The gender of the person ('male' or 'female')",
"type":"string"
},
"equation":{
"description":"The equation to use for BMR calculation ('harris_benedict' or 'mifflin_st_jeor')",
"type":"string"
}
}
},
"required":[
"weight",
"height",
"age",
"gender",
"equation"
]
},
{
"name":"calculate_tdee",
"description":"Calculates the Total Daily Energy Expenditure (TDEE) for a person",
"parameters":{
"type":"object",
"properties":{
"bmr":{
"description":"The BMR of the person",
"type":"number"
},
"activity_level":{
"description":"The activity level of the person",
"type":"string"
}
}
},
"required":[
"bmr",
"activity_level"
]
}
]
与代理互动:用户对话
在将定义的函数封装到 FitnessAgent 中之后,我们现在可以模拟与我们的机器人对话。通过使用 FitnessAgent 类的 ask()
方法,我们可以轻松地将用户查询输入到我们的机器人中,并打印出生成的响应。
例如,我们可以询问机器人一些关于常见食物的营养信息:
# Define a question
user_input = "What is the nutritional value of a banana?"
# Get raw chat response
response = fitness_agent.ask(user_input)
# Print final response
print(response['choices'][0]['message']['content'])
输出:
A 100-gram serving of banana typically contains:
- Calories: 89.4
- Total Fat: 0.3 grams, of which Saturated Fat is 0.1 grams
- Protein: 1.1 grams
- Sodium: 1 milligram
- Potassium: 22 milligrams
- Cholesterol: 0 milligram
- Total Carbohydrates: 23.2 grams, of which Dietary Fiber is 2.6 grams and Sugars are 12.3 grams
These values may vary based on the exact size and ripeness of the banana. Also note, bananas are a good source of dietary potassium and vitamin C.
在这个示例中,FitBot 有效地使用了 get_nutritional_info()
函数来获取并显示香蕉的营养价值。
FitBot 也可以处理更复杂的请求。例如,如果提供所需的数据,它可以计算一个人的总每日能量消耗 (TDEE):
# Define a question
user_input = "What is the TDEE of a 30-year-old man, who is 180 cm tall, weighs 80 kg, and exercises 3 times a week?"
# Get raw chat response
response = fitness_agent.ask(user_input)
# Print final response
print(response['choices'][0]['message']['content'])
输出:
The Total Daily Energy Expenditure (TDEE) of a 30-year-old man who is 180 cm tall, weighs 80 kg, and exercises 3 times a week with "moderate" exercise according to the Harris-Benedict equation would be approximately 2574 calories a day. This is a rough estimate and individual results could vary based on multiple factors like metabolism speed, specific physical activity, and more.
Here's a simple breakdown of the process:
1\. Calculating Basal Metabolic Rate (BMR) - This is the number of calories a person would burn just by existing, without any activity.
For men, the Harris-Benedict equation to calculate BMR is
BMR = 88.362 + (13.397 * weight in kg) + (4.799 * height in cm) - (5.677 * age in years)
For the given values, the BMR comes to around 1783 calories.
2\. Adjusting for Activity - The BMR can thus be adjusted for physical activity level using the following multipliers:
- Sedentary (little or no exercise): BMR * 1.2
- Lightly active (light exercise/sports 1-3 days/week): BMR * 1.375
- Moderately active (moderate exercise/sports 3-5 days/week): BMR * 1.55
- Very active (hard exercise/sports 6-7 days a week): BMR * 1.725
- Super active (very hard exercise/physical job & exercise 2x/day): BMR * 1.9
For someone who exercises 3 times a week, we could take the multiplier to be at 1.55 (Moderately active).
Hence, TDEE = BMR * Activity Multiplier = 1783 * 1.55 = approximately 2764 calories.
Note: The actual total caloric needs may be slightly higher or lower as this calculation doesn't take into account muscle to fat ratio, specific types of activities, etc.
让我们将这个输出与基于 GPT-4 的原始 ChatGPT 进行比较:
TDEE, or Total Daily Energy Expenditure, represents the total number of calories a person needs to consume in a day to maintain their current weight, considering their physical activity level.
We first need to calculate the Basal Metabolic Rate (BMR), which is the amount of energy expended while at rest. There are several equations to calculate BMR, one of the commonly used ones is the Mifflin-St. Jeor equation:
For men: BMR = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) + 5
So, for your case:
BMR = 10 * 80kg + 6.25 * 180cm - 5 * 30y + 5
BMR = 800 + 1125 - 150 + 5 = 1780 kcal/day
This is the amount of calories needed to keep the body functioning while at rest.
To include physical activity in the equation, we multiply the BMR by the Physical Activity Level (PAL). For someone who exercises 3 times a week, a common PAL is 1.375 (moderate exercise).
So, TDEE = BMR * PAL
TDEE = 1780 kcal/day * 1.375 = 2447.5 kcal/day
So, a 30-year-old man who is 180 cm tall, weighs 80 kg, and exercises 3 times a week would need approximately 2448 calories per day to maintain his current weight. Please note this is just an estimate; actual caloric needs can vary based on many factors, including genetics and body composition. Always consult a healthcare provider or a registered dietitian for personalized advice.
将聊天机器人与用户界面集成
现在我们已经构建了健身代理并为其配备了有用的功能,我们希望将其展示在一个易于使用的界面中。
为此,我们使用 Gradio,这是一个 Python 库,允许我们迅速便捷地创建可共享的基于网页的用户界面。在本节中,我们将带你了解如何将聊天机器人与 Gradio 用户界面集成。
这是我们界面的整体结构:
def main():
openai_api_key = gr.components.Textbox(
lines=1,
label="Enter OpenAI API Key",
type="password",
)
nut_api_key = gr.components.Textbox(
lines=1,
label="Enter Nutrition API Key",
type="password",
)
question = gr.components.Textbox(
lines=3,
label="Enter your message",
)
output_history = gr.outputs.HTML(
label="Updated Conversation",
)
inputs = [
openai_api_key,
nut_api_key,
question,
]
iface = gr.Interface(
fn=partial(get_response),
inputs=inputs,
outputs=[output_history],
title="Fitness Agent",
description="A simple chatbot using a Fitness Agent and Gradio with conversation history",
allow_flagging=False,
)
iface.launch()
if __name__ == "__main__":
main()
这是我们的 main
函数,脚本的入口点。我们首先创建文本框,让用户输入他们的 OpenAI API 密钥和 Nutrition API 密钥。这些密钥设置为 password
类型以隐藏输入。接着,我们提供一个文本框供用户提问。机器人的回应将以 HTML 格式显示在标记为“Updated Conversation”的区域。
输入和输出随后传递给 Gradio 界面,脚本运行时启动该界面。
get_response
函数与健身代理互动:
def get_response(openai_api_key, nut_api_key, user_input, action=None):
set_openai_api_key(openai_api_key)
set_nut_api_key(nut_api_key)
fitness_agent = FitnessAgent(openai_api_key, nut_api_key)
# Get raw chat response
fitness_agent.ask(user_input)
memory = fitness_agent.agent.chat_history
# Iterate through messages in ChatMessageHistory and format the output
updated_conversation = '<div style="background-color: hsl(30, 100%, 30%); color: white; padding: 5px; margin-bottom: 10px; text-align: center; font-size: 1.5em;">Chat History</div>'
logger.info(memory)
for i, message in enumerate(memory):
if i != 0:
if message['role'] == 'user':
prefix = "User: "
background_color = "hsl(0, 0%, 40%)" # Dark grey background
text_color = "hsl(0, 0%, 100%)" # White text
else:
prefix = "Chatbot: "
background_color = "hsl(0, 0%, 95%)" # White background
text_color = "hsl(0, 0%, 0%)" # Black text
updated_conversation += f'<div style="color: {text_color}; background-color: {background_color}; margin: 5px; padding: 5px;">{prefix}{message["content"]}</div>'
return updated_conversation
在 get_response
中,我们使用 set_openai_api_key
和 set_nut_api_key
函数设置 OpenAI 和 Nutrition API 密钥,然后初始化我们的健身代理。接着,我们调用代理的 ask
方法,传入用户的问题,并存储对话历史记录。对话历史中的每条消息都被格式化为 HTML 字符串,并添加到 updated_conversation
。这个 HTML 字符串被返回并显示在 Gradio 界面中。
结果界面的概览
在整合了必要的计算和对话逻辑,并将这些内容封装到一个视觉上令人愉悦的 Gradio 用户界面中之后,我们的 FitBot 已经准备好与用户互动了!
最终界面如下所示:
图 2:聊天机器人的 Gradio 用户界面。图片由作者提供。
在界面上,你会看到三个输入框,可以在其中输入 OpenAI 和 API Ninjas 营养端点的必要密钥,以及用户对健身代理的消息。
摘要
本文详细描述了 FitBot 的创建,这是一个使用 OpenAI 的 GPT-4 的综合健身代理,一个能够理解和响应复杂用户查询的强大 AI 模型。
我们从构建计算关键健康指标的函数开始,如基础代谢率(BMR)、每日总能量消耗(TDEE)和体重指数(BMI)。这些计算构成了健身代理提供准确和量身定制的健身和营养建议的基础。
接下来,我们集成了 API Ninjas 的营养端点。这使得健身代理能够访问并提供准确的营养信息,这是任何全面健身和饮食计划的关键组成部分。
我们展示了如何构建对话逻辑,使健身代理更加互动。它能够处理对话流程,使其能够回答各种用户查询,并有效地引导用户进行健身之旅。
最后,我们使用 Gradio 将所有这些功能封装到一个视觉上吸引人的用户界面中。结果是一个不仅智能而且用户友好的健身代理,提供清晰且全面的建议,格式易于理解。
概况:在这篇文章中,我们构建了 FitBot,一个使用 OpenAI 的 GPT-4 的健身代理,能够提供个性化的健身和营养建议。我们实现了计算关键健康指标(BMR、TDEE、BMI)的功能,集成了一个营养 API,以获取准确的饮食信息,并通过 Gradio 封装了一个用户友好的界面。这个项目展示了 AI 在健康和健身领域的强大功能,简化了复杂的计算,提供了个性化建议,并通过吸引人的用户界面传递所有信息。
感谢阅读!
- 关注我在 Linkedin!
五个免费且可靠的天气数据来源
原文:
towardsdatascience.com/five-free-and-reliable-weather-data-sources-20b9ea6afac9
高质量数据以改善和增强你的建模过程
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 5 月 31 日
--
图片由 NOAA 提供,来自 Unsplash
天气以从明显到微妙和意外的方式影响人类决策。能源供应商需要数据来确保当电网上的成千上万台空调启动时,电力供应能够满足需求。纽约市的 Citibike 骑行需求在开始下雨时急剧下降。零售商甚至使用天气数据在季节性寒冷天气时向消费者推销感冒和流感药物。
大量的天气数据由政府和学术机构收集,这意味着在进行分析和建模时,你所需的数据通常是免费的。在美国尤其如此,因为由国家海洋和大气管理局 (NOAA) 收集的数据,包括国家天气服务 (NWS),是公共领域数据。
许多高质量的免费数据往往隐藏在难以找到的过时托管区域、FTP 服务器中,或者在 Google 的两页深处,背后有一大堆私人公司。例如,NOAA 的许多数据仅通过购物车流程提供,你可能需要在开始查找之前知道你想要的确切数据集。获取这些数据的难度意味着你最容易的选择通常是具有 API 和高搜索排名的私人数据提供商。这些来源可以很棒,但通常有严格的免费层级限制,如速率限制、历史时间限制、分辨率限制等。因此,为了帮助你寻找数据,我整理了五个高质量且可靠的免费数据来源。
国家环境信息中心 (NCEI)
数据集 | 气候数据在线 (CDO) | 国家气候数据中心 (NCDC) (noaa.gov)
NCEI 隶属于国家海洋和大气管理局(NOAA),提供了地球上最重要的环境数据档案之一。他们提供关于大气、沿海、海洋和地球物理参数的综合信息。你可以以各种格式下载历史天气模式、古气候学、太阳事件等数据。
如果你访问上述数据集链接,一个很好的起点是“遗留应用程序”下的全球小时数据。你将能够获取全球大多数主要机场天气站的每小时数据,以及美国的每个官方站点(主要但不总是机场)。如果你试图为特定地点增加数据,可以从最近的机场开始。
另一个有趣的数据集是风暴事件数据库,它不仅提供关于龙卷风和冰雹的信息,还记录了如佛罗里达的雪等异常事件,以及导致人员伤亡的气象相关事件,如雪崩。
风暴报告数据,通过 NCEI 风暴事件数据库以原始形式提供。图片来源:NOAA/NWS 风暴预警中心
气候规范等长期数据可以在气候数据在线下找到,特别适用于基于季节性的项目。在同一部分,你还可以查看天气雷达数据,这是来自 NWS 雷达安装的反射率数据。如果你在寻找像超本地降雨这样的数据,雷达数据是最好的选择,而且无论好坏,都将在使其可用于代码分析方面给你带来重大技术挑战。
如前所述,NOAA 数据属于公共领域,因此在此收集的任何内容都可以用于任何目的,包括商业用途。
欧洲中期天气预报中心(ECMWF)
ECMWF 是一个独立的组织,总部位于英国,由多个欧洲国家支持。它提供中期到长期的天气预报,并通过 ECMWF Web API 允许免费访问其数据集。他们的 ERA5 数据集对于气候研究特别有价值,提供了从 1979 年至今的详细大气、陆地表面和海洋波浪信息。
与 NOAA 观测数据不同,ERA5 是所谓的“再分析数据”。与观测数据相比,再分析数据本质上是模型插值的天气视图。对于 ERA5,数据是全球性的,分辨率为 0.25 度。每个数据点位置都有多个垂直层可用,因此你可以创建自 1979 年以来的任意时刻的大气垂直剖面。
全球东向风速,从 ERA5 数据中得出。来源:Copernicus 气候变化服务信息 2023
对我们这些数据人员来说,这些数据通过一个优秀的 API 提供,该 API 访问 ECMWF Copernicus 数据存储(CDS)。使用该服务,你需要注册并获取 API 密钥。使用 API 的一个专业技巧是下载 NetCDF 格式的数据,并使用 xarray Python 模块将其读取到 pandas 数据框中。
ECMWF 在这个列表中排名很高,因为它是一个高质量且受推崇的数据来源。然而,你必须小心理解他们提供的数据的使用条款,因为他们不像美国政府来源那样在公共领域下运营。在公开访问的内容中,有些项目受自身使用条款的限制。这些项目要么标明了谁控制该项目的权利,并在某些情况下提供了点击使用许可的链接,要么在图片说明中列出了使用限制。开放数据页面是一个很好的起点,提供了可用数据的摘要、交付方式和明确的许可要求链接。
国家气象局(NWS)
如果你在美国,并且曾经在电视上或手机上看到过天气警报,那是 NWS 的一位气象预报员发布的。如果你需要访问当前数据,例如,你有一个实时运行的模型或仪表板,这就是你需要开始的地方。
该 API 提供了对包括温度、降水、预报和波高在内的当前条件的访问,涵盖了整个美国及其领土水域。全球范围内也可以获得更有限的当前条件报告。文档和访问详情可以在上面链接的 NWS 官方网站上找到。
与 NCEI 数据一样,这属于公共领域,可以自由用于任何目的,但要小心不要违反速率限制。
过去三天的天气观测数据。来源:NWS 洛杉矶,USC/市区 LA 站点的观测数据
Open-Meteo
Open-Meteo 是一个合作项目,旨在提供开放、免费的天气数据访问。虽然它不是政府来源,但由于其致力于让气象数据免费获取,因此在这里列出。
他们提供了一个 API,提供对众多数据集的访问,包括预报、观测数据、历史数据和雷达图像。该 API 支持多种编程语言,使其成为将数据集成到应用程序中的一个好选择。
你可以将 API 用于个人和学术项目,但不能用于商业项目。
日本气象厅
日本气象厅(JMA)免费提供天气、气候和地震数据。虽然这些数据的细致程度和深度不及 NOAA 或 ECMWF 提供的数据,但却是获取日本本地天气和气候信息的最佳来源。
特别值得注意的是,JMA 的日环气象卫星是目前运行中最好的卫星之一,提供了极高质量的日本、东南亚、澳大利亚、新西兰以及太平洋岛屿的影像。
尽管 JMA 没有专用的 API,但包括观测、预报和警告在内的数据集以文本、XML 和图形格式等多种格式提供,可以在上面的链接或网站上相对容易找到。如果你深入挖掘,可能需要使用翻译工具,因为大部分信息是用日语写的。
希望这些资源对你有所帮助,你能找到你所需要的信息。祝你的项目好运!
你应该注意的五个数据泄露的隐藏原因
原文:
towardsdatascience.com/five-hidden-causes-of-data-leakage-you-should-be-aware-of-e44df654f185
以及它们如何破坏机器学习模型
·发表于 Towards Data Science ·8 分钟阅读·2023 年 4 月 11 日
--
数据泄露是一个隐蔽的问题,通常困扰机器学习模型。泄露这个术语指的是测试数据泄漏到训练集中。当模型在训练过程中接触到不应有的数据时,就会发生这种情况,导致过拟合和在未见过数据上的表现不佳。这就像用考试答案来训练学生一样——他们在特定的考试中表现出色,但在其他考试中表现就不那么好。机器学习的目标是创建能够泛化并在新的、未见过的数据上做出准确预测的模型。数据泄露破坏了这一目标,因此了解和准备应对它非常重要。在本文中,我们将深入探讨什么是数据泄露,它的潜在原因,以及如何通过使用 Python 和 scikit-learn 的实际示例以及研究中的案例来防止它。
数据泄露的后果
-
过拟合。 数据泄露的一个重大后果是过拟合。过拟合发生在模型被训练得过于贴合训练数据,以至于无法对新数据进行泛化。当数据泄露发生时,模型在开发过程中使用的训练集和测试集上的准确率很高。然而,当模型被部署时,它的表现不会那么好,因为它无法将其分类规则泛化到未见过的数据。
-
误导性的性能指标。 数据泄露还可能导致误导性的性能指标。模型可能会表现出高准确率,因为它在训练过程中看到了部分测试数据。因此,很难评估模型并理解其性能。
划分前的数据泄露
我们呈现的第一个案例是最简单的,但可能是最常见的:在训练/测试划分之前进行预处理。
你想使用 StandardScaler 来标准化数据,因此你加载数据集,进行标准化,创建训练集和测试集,然后运行模型。对吗?错了。
0.745
均值和标准差是在整列上计算的,因此它们包括了测试集中的信息。使用这些值进行标准化处理意味着测试数据正在泄漏到训练数据中。
解决方案:管道
0.73
在这个版本中,使用了管道来封装预处理步骤,然后仅在训练集上进行拟合和评估。在这种情况下,StandardScaler被用作预处理步骤,它通过减去均值并缩放到单位方差来标准化特征。当你调用fit方法时,sklearn 会分别标准化每个数据集。这确保了测试集不会用于指导预处理步骤,从而避免了数据泄露。
使用交叉验证时的数据泄露
第二个例子是一个非常常见的错误,通常被忽视。你的数据集是不平衡的,你已经了解过应该如何使用过采样来“修复”它。经过一些搜索,你发现了 SMOTE,这是一种使用最近邻生成新样本以平衡少数类的算法。我们将这个技术应用于名为 credit_g 的数据集,来自 PMLB 库。
数据集是不平衡的,类别之间的比例为 70/30。
ROC AUC score (baseline): 0.75 +/- 0.01
作为基线结果,我们展示了不应用任何变换的 AUC 分数。运行逻辑回归模型的平均 ROC AUC 分数为 0.75。
现在让我们应用 SMOTE。
1 700
0 700
Name: target, dtype: int64
ROC AUC score (with data leakage): 0.84 +/- 0.07
应用 SMOTE 后,你会很高兴地看到 AUC 分数从 0.75 提高到 0.84!然而,所有的光芒都不是金子:你刚刚造成了数据泄露。在上面的代码中,变换在运行交叉验证之前应用,这将训练和测试集分割到不同的折中。这是一个非常常见的场景,可能会误导初学者认为 SMOTE 提高了他们的模型性能。
现在让我们看一下修正后的代码,其中 SMOTE 在交叉验证划分之后应用。
ROC AUC score: 0.67 +/- 0.00
正确应用 SMOTE 实际上使模型更糟。
正如Samuele Mazzanti在他的文章你的数据集不平衡?什么都不要做!中强调的那样,过采样并不是处理不平衡数据集的必要手段。
时间序列中的数据泄露
时间序列数据具有独特的特性,使其与其他类型的数据不同,这可能导致在划分数据、准备特征和评估模型时出现特定的挑战。在这里,我们将详细阐述这些挑战,并建议最佳实践以最小化时间序列分析中的数据泄露。
不正确的训练-测试划分:在时间序列数据中,将数据集分为训练集和测试集时,必须保持观察的时间顺序。随机划分可能会引入数据泄漏,因为它可能将未来的信息包含在训练集中。为了避免这种情况,应使用基于时间的划分,确保训练集中的所有数据点都在测试集的数据点之前。你还可以使用时间序列交叉验证或前向验证等技术,以更准确地评估模型的性能。
特征工程:你应该避免使用在预测时无法获得的未来信息。例如,计算技术指标、滞后变量或滚动统计数据时,只应使用过去的数据,而不是未来的数据。为了在特征工程过程中防止数据泄漏,你可以使用诸如应用基于时间的窗口函数等技术,确保计算窗口只包含到预测时间为止的数据。这也适用于外部数据。有时,时间序列模型会结合可能包含未来信息的外部数据源。确保指标适当地滞后,以免提供未来的信息,并始终验证外部数据源是否与主要时间序列数据集保持相同的时间顺序。
图像数据中的数据泄漏
当处理医学数据时,通常会从同一患者那里获取多张图像。在这种情况下,你不能仅仅随机划分数据集来训练模型,因为你可能会不小心将同一人的图像放在训练集和测试集中。相反,你需要使用按受试者划分的方法。
那么,什么是按受试者划分?这意味着你将同一人的所有图像放在一起,要么在训练集中,要么在测试集中。这样,你的模型就不能通过从两个集合中的同一人的图像中学习而作弊。
有一项研究探讨了随机划分与按受试者划分之间的差异。他们在三个不同的数据集上进行测试,发现随机划分会导致测试准确度虚高,因为数据泄漏。另一方面,按受试者划分则能得到更准确的结果。使用的数据集如下:
AIIMS 数据集:包含来自 45 名受试者(22 名癌症患者和 23 名健康受试者)的 18,480 张健康和癌症乳腺组织的 2D OCT 图像。
Srinivasan 数据集:一个眼科学数据集,包括 3,231 张年龄相关性黄斑变性(AMD)、糖尿病性黄斑水肿(DME)和正常受试者的 2D OCT 图像,每类包括 15 名受试者。
Kermany 的数据集:一个大型开放获取的眼科数据集,包含来自 5,319 名患者的脉络膜新生血管(CNV)、糖尿病性黄斑水肿(DME)、视网膜上皮下层(drusen)和正常视网膜图像。该数据集有不同版本,图像数量、组织和训练与测试集之间的数据重叠有所不同。
结果不言自明。
不同分割策略的比较。来源。
模型通过 Matthews 相关系数进行评估,定义如下:
Matthews 相关系数。图像由作者提供。
正如你可以想象的那样,当我们随机分割数据时,得到的分数好得令人难以置信。这是因为来自同一个人的图像看起来非常相似,因此模型在识别训练数据中已经见过的人时更容易。在现实世界中,我们需要能够可靠识别新患者疾病的模型。
数据泄露是一个常见的问题,即使是最熟练的数据科学家在构建机器学习模型时也会受到影响。接下来,我们将看看另一个研究案例。2017 年,Andrew Ng 及其团队发表了一篇开创性的论文,标题为“CheXNet:利用深度学习在胸部 X 光片中进行放射科医师级的肺炎检测。” 这篇论文介绍了一种利用深度学习检测胸部 X 光片中肺炎的算法,其性能与专家放射科医师相当。以下图像来自论文。
ChestXNet 原始论文。第一个版本。来源。
你注意到有什么问题吗?
在这项研究的第一个版本中,他们训练模型时随机划分数据。由于包含了同一患者的多个扫描图像,这种潜在的数据泄露引发了对 CheXNet 结果可靠性和泛化能力的担忧。作者认识到这个问题,后来发布了新版本,纠正了这个问题。以下图像来自修正版本。
ChestXNet 原始论文。最新版本。来源。
结论
数据泄露是一个隐蔽的问题,会在开发的各个阶段影响机器学习模型。正如我们在本文中探讨的,它可能导致过拟合、误导性的性能指标,以及最终一个对未见数据无法良好泛化的模型。无论你是在处理表格数据、时间序列还是图像,都需要意识到这一点,以构建成功的模型。以下是本文的一些关键要点:
-
如果你的模型在进行某些更改后突然表现过于出色,检查是否有数据泄露总是个好主意。
-
避免在将数据集拆分为训练集和测试集之前对整个数据集进行预处理。相反,使用管道来封装预处理步骤。
-
在使用交叉验证时,对如过采样或其他任何变换技术要小心。只对每个折中的训练集应用这些技术,以防止数据泄露。
-
对于时间序列数据,保持观察的时间顺序,并使用诸如基于时间的拆分和时间序列交叉验证等技术。
-
对于图像数据或具有多个记录的同一受试者的数据集,使用每个受试者的拆分以避免数据泄露。
牢记这些要点,你将能够更好地构建更强大和更准确的机器学习模型。
喜欢这篇文章?通过订阅我的新闻通讯,每周获取数据科学面试问题,《数据面试》。
你也可以在 LinkedIn上找到我。
产品管理中的五种强大优先级排序技巧
原文:
towardsdatascience.com/five-powerful-prioritization-techniques-from-product-management-c44cd1f7e2f3
数据科学家如何从产品经理构建产品、关注以客户为中心的思维以及确保产品成功中获得启发。
·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 2 日
--
图片由 Dayne Topkin 提供,来源于 Unsplash
作为数据科学家、分析师或任何开发产品或解决方案的人,你可以借鉴产品经理用于优先排序需求的工具。在资源和时间有限的情况下,专注于能够为客户带来最大价值的功能和需求是至关重要的。以下是我最喜欢的五种功能和需求优先级排序技巧:
-
用户故事映射
-
加权优先级矩阵
-
重要性与难度矩阵
-
购买功能
-
Kano 模型
每种技巧都有其优点和最适用的情况。尝试所有技巧,了解何时应用每种技巧。让我们逐一了解每种技巧的基本操作以及何时使用它们。
用户故事映射
用户故事映射 是一种通过地图可视化产品功能和需求的强大技巧。它帮助你理解用户的旅程,并根据功能在实现目标中的重要性来优先排序。
执行用户故事映射:
-
开始时将产品拆分成较小的用户故事。这些故事应描述用户的目标、需求和痛点。
-
组织用户故事,将分组表示为不同的活动及用户完成该活动所需执行的工作流程。
-
根据用户故事对用户旅程的重要性进行优先排序(从上到下)。
提示:用户故事映射是一个迭代过程,应根据用户反馈和变化的业务优先级定期更新和修订。此外,涉及你的团队和利益相关者进行映射,以确保对齐和认同。
何时使用:当你开始一个新项目并希望了解用户的工作流程时,这种方法效果很好。这种方法帮助你框定问题,你将肯定会发现一些你之前没有考虑到的领域。
作者提供的图片
加权优先级矩阵
加权优先级矩阵是一种基于多个标准对特性进行优先排序的技术。它涉及为每个标准分配权重,并根据每个特性满足每个标准的程度进行评分。然后,将结果得分乘以权重,以计算每个特性的最终得分。
执行加权优先级矩阵的方法:
-
确定你想用来评估特性的标准。
-
根据每个标准的相对重要性为其分配权重。
-
根据每个标准的符合程度对每个特性进行 1 到 n 的评分。你可以使用任何你想要的评分标准,但要保持一致。
-
将每个得分乘以其对应的权重,并将结果相加,以计算每个特性的最终得分。作为奖励,你可以平方得分,以给更高的得分更多的权重,例如,平方加权和。
=($B$1*B3*B3)+($C$1*C3*C3)+($D$1*D3*D3)+($E$1*E3*E3)...
在这里下载我的 Excel 示例:加权优先级矩阵。
提示:使用加权优先级矩阵时,涉及利益相关者和团队成员的标准选择和权重过程,以确保对齐和认同。此外,要注意你为每个特性打分时尽量客观。你可以将自定义标准(如它如何符合特定业务目标)与传统标准(如风险、收入、紧急程度、努力程度等)混合使用。
何时使用:当你想以更有条理的方式为一长串特性打分时,这种方法非常适用。通过最终得分进行排序,给你一个堆叠的排名列表。因为你与团队合作进行评分系统,它可以减少关于优先级的一些主观性。
作者提供的图片
重要性与难度矩阵
重要性难度矩阵,也称为价值努力矩阵,是一种根据特性的重要性和难度来优先排序的技术。它涉及将特性映射到一个二维矩阵上,一个轴表示特性的优先级,另一个轴表示实施的难度。
执行重要性难度矩阵的方法:
-
确定你想要优先排序的特性。
-
将每个特性在水平轴上从最不重要到最重要排序。确保没有两个特性在同一列中。
-
接下来,将每个特性在垂直轴上按难度从低到高排序。确保没有两个特性在同一行。
提示:在使用重要性困难矩阵时,涉及你的团队和利益相关者进行评分,以确保对优先级的对齐和认同。此外,排序过程是最重要的部分,你正在对优先级达成一致。此外,每个特性必须在不同的列或行中,确保没有两个特性被视为同等重要或困难。
何时使用:当你有大约十个项目需要排序时,这是一个很好的协作练习。这个减少的集合可以是你已筛选并希望获得绝对优先级的项目列表。
作者提供的图片
购买特性
购买特性是一种技术,涉及给客户一定数量的资金来购买他们希望在产品中看到的特性。客户可以将预算分配到他们认为最重要的特性上。这种技术可以帮助产品经理了解客户最需求的特性,并据此进行优先级排序。
要执行“购买特性”:
-
确定你想要优先考虑的特性。
-
为每个特性分配一个货币价值,并给客户一个“资金”预算来支出。
-
你可以使用实物或数字代币来代表资金,让客户实际“购买”他们想要的特性。一旦所有客户花完预算,测试结果以确定哪些特性最受欢迎。
提示:为了确保“购买特性”的成功,请为流程设定明确的规则和指导方针。对预算及每个特性的费用保持透明。确保涉及到代表性客户群体以获得广泛的视角。此外,考虑向不同客户群体提供不同的预算,因为某些客户可能对某些特性更看重。
何时使用:当你直接与客户或利益相关者合作时,他们坚持认为一切都是高优先级的。“购买特性”技术迫使客户决定他们真正想要什么。它在一个可以设置非常困难的特性值高于一个人分配金额的群体中效果很好,这会强迫一个协作讨论,其中多个人需要将资源汇集在一起。
作者提供的图片
Kano 模型
Kano 模型是一种识别哪些特性将为客户提供最大价值的技术。它将特性分类为不同类型:
-
必备特性:这些是客户期望的基本要求。这些特性如你车上的刹车,你不会买没有这些特性的车。
-
性能特性:这些为客户提供递增的价值。这些特性可以是更好的燃油效率或更强大的引擎。
-
令人惊喜的功能:这些是能够给客户带来惊喜的意外功能。这些功能包括加热方向盘或天窗。
执行 Kano 模型:
-
确定客户需求:使用 Kano 模型的第一步是识别与您的产品或服务相关的客户需求,这可以通过调查、焦点小组或客户访谈来完成。
-
分类客户需求:一旦识别出客户需求,你需要将它们分类为三类:基本需求、表现需求和令人惊喜的功能。
-
确定满意度水平:接下来,你需要确定客户对每个识别出的需求的满意度水平。可以使用李克特量表(例如,1-5)来测量客户满意度。
-
绘制数据:一旦你有了每个需求的满意度水平,将它们绘制在 Kano 模型图上。在横轴上绘制需求的表现或实施水平,在纵轴上绘制客户的满意度水平。
-
分析结果:根据绘制的数据,你可以识别出每个类别(基本需求、表现需求和令人惊喜的功能)中的需求。此外,你还可以识别出当前未满足并需要改进的需求。
-
制定行动计划:根据分析,制定一个行动计划,以满足客户的需求,包括改善基本需求、优化表现需求或投资于令人惊喜的功能。
-
重复过程:Kano 模型不是一次性的练习。为了确保满足客户需求,定期重复这一过程非常重要,以识别客户需求和满意度水平的变化。
提示:请记住,客户需求和期望可能会随时间变化,因此定期重新评估功能的分类非常重要。此外,要小心不要过于关注令人惊喜的功能而忽略基本需求,因为关注基本需求可以带来客户满意度。此外,在进行调查或访谈时,使用足够大的样本量以确保数据能够代表您的客户群体。
何时使用:这是优先排序的更全面技术之一,掌握它可能需要一段时间。我建议阅读Folding Burritos 上的 Kano 模型完整指南。
结论
优先级排序是任何构建产品的人都需要掌握的关键技能。优先级排序要求深入了解客户的需求、业务目标和资源。我们讨论的五种技术——用户故事映射、加权优先级矩阵、重要性与难度矩阵、购买功能和卡诺模型——提供了不同的优先级排序方法。尽管如此,它们的终极目标都是最大化客户和业务的价值。记得在优先级排序过程中涉及你的团队和利益相关者,定期重新评估你的优先级,并专注于为客户提供价值。有效运用这些技术,你可以创造出让客户满意并推动业务成功的产品。
如果你喜欢阅读这样的故事,并希望支持我作为作家,考虑注册成为 Medium 会员。每月 5 美元,享有对数千篇文章的无限访问。如果你使用 我的链接注册,我将获得一小笔佣金,但不会额外产生费用。
五种实际应用 LSTM 模型于时间序列的案例,附代码
如何在多个不同的时间序列背景下实现高级神经网络模型
·
关注 发表在 Towards Data Science ·11 min read·Sep 22, 2023
--
图片来源于 Andrew Svk 在 Unsplash
当我在 2022 年 1 月写了探索 LSTM 神经网络模型在时间序列中的应用时,我的目标是展示如何使用我开发的时间序列库scalecast在 Python 中轻松实现这个先进的神经网络。我没想到它会被观看超过数万次,并在我发布后超过一年内在 Google 搜索“lstm forecasting python”时排名第一(今天检查时,它仍然是第二)。
我并没有尝试引起对那篇文章的过多关注,因为我从未认为,也仍然认为,它不是很好。它从未打算成为实现 LSTM 模型最佳方法的指南,而只是简单探讨其在时间序列预测中的实用性。我试图回答诸如:当你使用默认参数运行模型时会发生什么,当你以这种或那种方式调整参数时会发生什么,它在某些数据集上被其他模型击败的难易程度等问题。然而,根据博客文章、Kaggle 笔记本,甚至我不断看到的Udemy 课程,这篇文章的代码被逐字复制,很明显许多人把它当作前者的价值,而不是后者。我现在明白我没有清晰地表达我的意图。
今天,为了扩展那篇文章,我想展示如何应用 LSTM 神经网络模型,或者至少是我如何应用它,以充分发挥其在时间序列预测问题中的价值。自从我写了第一篇文章以来,我们已经能够为 scalecast 库添加许多新的创新功能,使得使用 LSTM 模型更加无缝,我将在这里探讨一些我最喜欢的功能。我认为 LSTM 有五种应用会在这个库中表现得非常出色:单变量预测、多变量预测、概率预测、动态概率预测和迁移学习。
在开始之前,请确保在终端或命令行中运行:
pip install --upgrade scalecast
为本文开发的完整笔记本位于这里。
最后一点:在每个示例中,我可能会将“RNN”和“LSTM”互换使用。或者,RNN 可能会显示在 LSTM 预测的某个图表上。长短期记忆(LSTM)神经网络是一种递归神经网络(RNN),具有额外的记忆相关参数。在 scalecast 中,rnn
模型类可以用来拟合从tensorflow移植过来的简单 RNN 和 LSTM 单元。
1. 单变量预测
使用 LSTM 模型最常见且最明显的方式是处理简单的单变量预测问题。尽管该模型拟合了许多参数,使其足够复杂以有效学习任何给定时间序列中的趋势、季节性和短期动态,但我发现它在处理平稳数据(即不表现出趋势或季节性的数据显示)时效果更好。因此,使用可在Kaggle上获取的航空乘客数据集,我们可以仅通过去趋势和去季节性处理数据,使用相当简单的超参数来创建准确可靠的预测:
transformer = Transformer(
transformers = [
('DetrendTransform',{'poly_order':2}),
'DeseasonTransform',
],
)
我们还要确保在完成后将结果恢复到其原始水平:
reverter = Reverter(
reverters = [
'DeseasonRevert',
'DetrendRevert',
],
base_transformer = transformer,
)
现在,我们可以指定网络参数。在这个示例中,我们将使用 18 个滞后、一个层、一个 tanh 激活函数和 200 个训练周期。随意探索你自己的、更好的参数!
def forecaster(f):
f.set_estimator('rnn')
f.manual_forecast(
lags = 18,
layers_struct = [
('LSTM',{'units':36,'activation':'tanh'}),
],
epochs=200,
call_me = 'lstm',
)
将所有内容结合成一个管道,运行模型,并从视觉上查看结果:
pipeline = Pipeline(
steps = [
('Transform',transformer),
('Forecast',forecaster),
('Revert',reverter),
]
)
f = pipeline.fit_predict(f)
f.plot()
plt.show()
作者提供的图片
足够好,比我在另一篇文章中展示的任何内容都要好得多。要扩展这个应用,你可以尝试使用不同的滞后阶数,将季节性以傅里叶项的形式添加到模型中,寻找更好的序列转换,并通过交叉验证来调整模型的超参数。后续部分将演示其中的一些方法。
2. 多变量预测
假设我们有两个序列,我们预计它们会一起变化。我们可以创建一个 LSTM 模型,在进行预测时考虑这两个序列,希望提高模型的整体准确性。这就是所谓的多变量预测。
在这个示例中,我将使用可在Kaggle上获取的鳄梨数据集。它测量了不同美国地区的鳄梨价格和销售数量。根据经济理论,我们知道价格和需求是密切相关的,因此使用价格作为领先指标,我们可能会比仅使用历史需求更准确地预测鳄梨的销售量。
我们首先要做的是转换每个序列。我们可以通过运行以下代码来搜索一组“最佳”转换(即在样本外得分的转换):
data = pd.read_csv('avocado.csv')
# demand
vol = data.groupby('Date')['Total Volume'].sum()
# price
price = data.groupby('Date')['AveragePrice'].sum()
fvol = Forecaster(
y = vol,
current_dates = vol.index,
test_length = 13,
validation_length = 13,
future_dates = 13,
metrics = ['rmse','r2'],
)
transformer, reverter = find_optimal_transformation(
fvol,
set_aside_test_set=True, # prevents leakage so we can benchmark the resulting models fairly
return_train_only = True, # prevents leakage so we can benchmark the resulting models fairly
verbose=True,
detrend_kwargs=[
{'loess':True},
{'poly_order':1},
{'ln_trend':True},
],
m = 52, # what makes one seasonal cycle?
test_length = 4,
)
从这个过程中推荐的变换是季节调整,假设 52 个周期构成一个季节,以及一个鲁棒缩放(对异常值鲁棒的缩放)。然后,我们可以在系列上拟合该变换,并调用单变量 LSTM 模型,以便与多变量模型进行基准对比。这一次,我们将使用超参数调优过程,生成可能的激活函数、层大小和丢弃值的网格。
rnn_grid = gen_rnn_grid(
layer_tries = 10,
min_layer_size = 3,
max_layer_size = 5,
units_pool = [100],
epochs = [25,50],
dropout_pool = [0,0.05],
callbacks=EarlyStopping(
monitor='val_loss',
patience=3,
),
random_seed = 20,
) # creates a grid of hyperparameter values to tune the LSTM model
这个函数提供了一种将可管理的网格输入到我们的对象中的良好方式,同时也有足够的随机性,以便有一个好的参数候选集。现在我们拟合单变量模型:
fvol.add_ar_terms(13) # the model will use 13 series lags
fvol.set_estimator('rnn')
fvol.ingest_grid(rnn_grid)
fvol.tune() # uses a 13-period validation set
fvol.auto_forecast(call_me='lstm_univariate')
为了将其扩展到多变量背景下,我们可以用与其他系列相同的变换集来转换价格时间序列。然后,将 13 个价格滞后值输入Forecaster
对象中,并拟合一个新的 LSTM 模型:
fprice = Forecaster(
y = price,
current_dates = price.index,
future_dates = 13,
)
fprice = transformer.fit_transform(fprice)
fvol.add_series(fprice.y,called='price')
fvol.add_lagged_terms('price',lags=13,drop=True)
fvol.ingest_grid(rnn_grid)
fvol.tune()
fvol.auto_forecast(call_me='lstm_multivariate')
我们还可以基准化一个简单模型,并在原始系列水平绘制结果,以及样本外测试集:
# naive forecast for benchmarking
fvol.set_estimator('naive')
fvol.manual_forecast()
fvol = reverter.fit_transform(fvol)
fvol.plot_test_set(order_by='TestSetRMSE')
plt.show()
由作者提供的图像
从三种模型的视觉聚类来看,这一系列数据的准确性主要得益于所应用的变换——这也是为什么简单模型与 LSTM 模型的表现如此接近的原因。不过,LSTM 模型确实有所改进,多变量模型的得分和 r 平方为 38.37%,单变量模型为 26.35%,而基准为-6.46%。
由作者提供的图像
可能阻碍 LSTM 模型在这一系列数据上表现更好的一个因素是数据集的长度。仅有 169 个观察值,可能不足以让模型充分学习模式。然而,相较于某些简单模型的改进,任何提升都可以被视为成功。
3. 概率预测
概率预测是指模型不仅能够进行点预测,还能够提供预测偏差的估计。概率预测类似于使用置信区间进行预测,这一概念已经存在很长时间了。产生概率预测的一个新兴方法是通过将一致性置信区间应用于模型,利用校准集来确定实际未来点的可能分布。这种方法的优点在于可以适用于任何机器学习模型,无论该模型对输入或残差的分布做出什么假设。它还提供了对任何机器学习从业者都非常有用的覆盖保证。我们可以将一致性置信区间应用于 LSTM 模型以产生概率预测。
在这个例子中,我们将使用FRED上提供的月度住房开工数据集,这是一个经济时间序列的开放数据库。我将使用 1959 年 1 月到 2022 年 12 月的数据(768 个观察值)。首先,我们将再次搜索最佳的变换集合,但这次使用一个具有 10 个周期的 LSTM 模型来评分每次变换尝试:
transformer, reverter = find_optimal_transformation(
f,
estimator = 'lstm',
epochs = 10,
set_aside_test_set=True, # prevents leakage so we can benchmark the resulting models fairly
return_train_only = True, # prevents leakage so we can benchmark the resulting models fairly
verbose=True,
m = 52, # what makes one seasonal cycle?
test_length = 24,
num_test_sets = 3,
space_between_sets = 12,
detrend_kwargs=[
{'loess':True},
{'poly_order':1},
{'ln_trend':True},
],
)
我们将再次随机生成一个超参数网格,但这次我们可以将其搜索空间设置得非常大,然后在模型拟合后手动将其限制为 10 次尝试,以便在合理的时间内对参数进行交叉验证:
rnn_grid = gen_rnn_grid(
layer_tries = 100,
min_layer_size = 1,
max_layer_size = 5,
units_pool = [100],
epochs = [100],
dropout_pool = [0,0.05],
validation_split=.2,
callbacks=EarlyStopping(
monitor='val_loss',
patience=3,
),
random_seed = 20,
) # make a really big grid and limit it manually
现在我们可以构建和拟合管道:
def forecaster(f,grid):
f.auto_Xvar_select(
try_trend=False,
try_seasonalities=False,
max_ar=100
)
f.set_estimator('rnn')
f.ingest_grid(grid)
f.limit_grid_size(10) # randomly reduce the big grid to 10
f.cross_validate(k=3,test_length=24) # three-fold cross-validation
f.auto_forecast()
pipeline = Pipeline(
steps = [
('Transform',transformer),
('Forecast',forecaster),
('Revert',reverter),
]
)
f = pipeline.fit_predict(f,grid=rnn_grid)
因为我们在Forecaster
对象中预留了足够大小的测试集,所以结果自动为每个点估计提供了 90%的概率分布:
f.plot(ci=True)
plt.show()
图片由作者提供
4. 动态概率预测
之前的示例提供了一个静态的概率预测,其中预测的每个上界和下界距离点估计的距离与其他点的上界和下界相等。当预测未来时,直观上看,预测越远,误差的扩散范围就越宽——这一细微差别在静态区间中没有体现。通过使用回测,有一种方法可以实现更动态的概率预测。
回测是一个迭代地重新拟合模型、在不同预测范围内进行预测并测试其性能的过程。让我们以最后一个示例中指定的管道为例,对其进行 10 次回测。我们需要至少 10 次回测迭代来构建 90%置信区间:
backtest_results = backtest_for_resid_matrix(
f,
pipeline=pipeline,
alpha = .1,
jump_back = 12,
params = f.best_params,
)
backtest_resid_matrix = get_backtest_resid_matrix(backtest_results)
我们可以通过可视化分析每次迭代中残差的绝对值:
图片由作者提供
这个特定示例有趣之处在于,最大的误差通常不在预测的最后几个步骤上,而是在步骤 14-17 之间。这种情况可能发生在具有奇特季节性模式的序列中。异常值的存在也可能影响这种模式。不管怎样,我们可以利用这些结果现在用动态区间替换静态置信区间,这些动态区间在每一步都是符合的。
overwrite_forecast_intervals(
f,
backtest_resid_matrix=backtest_resid_matrix,
alpha=.1, # 90% intervals
)
f.plot(ci=True)
plt.show()
图片由作者提供
5. 转移学习
转移学习在我们希望在模型适配的上下文之外使用模型时很有用。我将演示其实用性的两个具体情景:在给定时间序列中有新数据可用时进行预测,以及对具有类似趋势和季节性的相关时间序列进行预测。
情景 1:来自同一序列的新数据
我们可以使用与之前两个示例相同的住房数据集,但假设已经过去了一段时间,我们现在有数据可用到 2023 年 6 月。
df = pdr.get_data_fred(
'CANWSCNDW01STSAM',
start = '2010-01-01',
end = '2023-06-30',
)
f_new = Forecaster(
y = df.iloc[:,0],
current_dates = df.index,
future_dates = 24, # 2-year forecast horizon
)
我们将重新制作管道,使用相同的转换,但这次使用转移预测,而不是适配模型的正常尺度预测过程:
def transfer_forecast(f_new,transfer_from):
f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)
f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')
pipeline_can = Pipeline(
steps = [
('Transform',transformer),
('Transfer Forecast',transfer_forecast),
('Revert',reverter),
]
)
f_new = pipeline_can.fit_predict(f_new,transfer_from=f)
尽管相关函数的名称仍然是fit_predict()
,但实际上在管道中没有拟合,只有预测。这大大减少了我们需要重新拟合和重新优化模型的时间。然后我们查看结果:
f_new.plot()
plt.show('Housing Starts Forecast with Actuals Through June, 2023')
plt.show()
图片由作者提供
情景 2:具有相似特征的新时间序列
对于第二种情景,我们可以使用一个假设的情况:希望利用在美国住房动态上训练的模型来预测加拿大的住房开工情况。免责声明:我不知道这是否真的一个好主意——这只是我想到的一个场景,用来演示如何完成这一任务。但我认为这可能会有用,而且相关代码可以转移到其他情况(例如,对于那些你拥有的短时间序列,其动态类似于你已经适配了表现良好的模型的较长序列)。在这种情况下,代码实际上与情景 1的代码完全相同;唯一的区别是我们加载到对象中的数据:
df = pdr.get_data_fred(
'CANWSCNDW01STSAM',
start = '2010-01-01',
end = '2023-06-30',
)
f_new = Forecaster(
y = df.iloc[:,0],
current_dates = df.index,
future_dates = 24, # 2-year forecast horizon
)
def transfer_forecast(f_new,transfer_from):
f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)
f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')
pipeline_can = Pipeline(
steps = [
('Transform',transformer),
('Transfer Forecast',transfer_forecast),
('Revert',reverter),
]
)
f_new = pipeline_can.fit_predict(f_new,transfer_from=f)
f_new.plot()
plt.show('Candian Housing Starts Forecast')
plt.show()
图片由作者提供
我认为该预测看起来足够可信,作为 LSTM 转移学习的一个有趣应用。
结论
对于许多预测用例,LSTM 模型可能是一个有趣的解决方案。在这篇文章中,我演示了如何使用 Python 代码将 LSTM 模型应用于五种不同的目的。如果你觉得这很有用,请在 GitHub 上给scalecast 点个星,并务必在 Medium 上关注我,以便及时了解包的最新动态。如果你有反馈、建设性的批评或对这段代码有任何疑问,随时可以通过电子邮件联系我:mikekeith52@gmail.com。
五个协作数据科学的软件工程原则
可重复的数据科学项目需要项目组织和干净的代码
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 1 月 13 日
--
传统的软件工程师在代码中制定规则。相比之下,数据科学家依赖于学习算法来分析数据中的模式。然而,分析项目仍然需要依赖传统代码,作为数据科学家,你可以从最初由软件工程领域首创的最佳实践中获益。
用一个比喻来说,假设你是一位厨师,正在准备一顿丰盛的晚餐。在厨房里,你有不同的食材和工具,你的工作是使用这些工具,并将食材以正确的方式组合在一起,做出美味的菜肴。你和你的顾客都希望你的菜肴既美味又不被烤焦或生煮。一间杂乱无章的厨房让其他人很难做出你的菜肴,因此一位优秀的厨师会花时间保持厨房整洁,标记食材,并在一套食谱中记录下菜肴的制作过程。
Louis Hansel 的照片,来自 Unsplash
同样,作为数据科学家,你希望我们的输出和洞察是正确且可重复的。在你的“代码厨房”中遵循最佳实践,有助于你创建有序的代码,这样你和其他人在未来可以继续进行分析项目,理解、扩展和重用它。
在这篇文章中,我们将讨论一些方法,以使你的“胶水代码”既可靠又高效且易于理解。当你开始在原型上编写代码时,可能不会优先考虑可维护性和一致性,但采用一种已经被证明有效的文化和工作方式,可以让你的原型更快地准备好投入生产。
“精心设计的数据科学代码可以帮助你解锁有价值的洞察。”
1. 使用标准且逻辑的项目结构
虽然数据科学旨在生成一系列洞察,如报告和可视化,但考虑生成这些洞察的程序代码的质量也是至关重要的。虽然数据实验可能会有偶然的结果,但你和你的潜在同事需要能够扩展实验并在未来重新运行它。
最好从相同的、一致的、逻辑的项目结构开始每个实验。任何查看项目的人都可以理解布局,而不需要 extensive documentation。组织良好的代码通常具有自我文档化的特点,因为它提供了上下文。
对每个项目使用相同的结构有助于可重复的协作,这意味着你可以对分析得出的结论充满信心。你是否曾尝试重现几个月前做的事情?你可能当时掌握了所有细节,但如果项目被搞得一团糟,返回时几乎要从头开始。
一致且可靠的项目使你更容易回顾和与他人分享,以便你的团队成员可以轻松维护和修改你的项目。
没有唯一的对或错的方法,但你应该采用一种语义文件夹结构,其中位置编码了含义(例如,配置、数据、文档、笔记本和源代码的文件夹)。这种方法使项目导航变得容易,因为对象的位置描述了其用途。
如果你在寻找灵感,可以查看 DrivenData 关于 CookieCutter Data Science 的页面,他们描述其为“一个逻辑性强、相对标准化但灵活的数据科学工作和分享的项目结构”。并且看看开源项目 Kedro,它基于 CookieCutter Data Science 的学习,提供可修改的项目启动模板以自定义你的模板。
图片来源:Max Komthongvijit 于 Unsplash
2. 通过依赖管理使你的环境可重复
大多数数据科学 Python 代码会导入第三方包,这些包提供可重用的功能。这些包有不同的版本,通常对其他 Python 包的特定版本有依赖关系。
依赖管理记录了你的项目的确切工作环境,因此可以通过安装等效的包集来在不同的干净环境中轻松重现设置。
一种选择是将每个包依赖和子依赖的列表写入文档中。推荐的方法是使用标准化、可重复、广泛接受的格式来列出这些信息,比如 pip install
的输入。
对于你的项目直接依赖的每个包,列出它及其需要的版本以进行“固定”。包可能会频繁更新;固定可以保护你免受更改引入的错误或不兼容变化的影响。
图片来源:Duane Mendes 于 Unsplash
虚拟环境
如果你在使用 Python,始终为你的项目使用虚拟环境,以保护环境免受全局环境潜在更改的影响。例如,如果你依赖于 Pandas 项目中 2021 年引入的一个功能,但另一个项目需要较旧的版本,那么如果你在一个全局空间中工作,就会出现冲突。
为每个项目保持一个独立的干净环境,例如使用 conda
或 venv
,可以确保更好的项目可重复性,因为你可以避免版本冲突。
在 YouTube 上了解 Python 虚拟环境工具和工作流:youtu.be/YKfAwIItO7M
3. 通过使代码可读来提高其可重用性
《代码整洁之道:敏捷软件工艺手册》是一本 2008 年的软件工程书籍,提出了无论使用什么编程语言或其目的,都应遵循的最佳实践。它提出了从头开始编写良好代码和改进糟糕代码的几个原则,并描述了“代码异味”,这些异味表明你的代码“出了问题”。
除了阅读书籍,你还可以找到许多视频、培训课程和书籍总结,根据你需要的详细程度。我的意图不是在这里复述所有内容,而是考虑书中描述的一个方面:代码可读性。
“代码被阅读的频率远高于编写的频率。”
虽然似乎没有单一来源的引述,但它常被归于 Python 编程语言的创建者 Guido van Rossum 和里程碑PEP8 文档的贡献者,该文档提供了编写可读代码的指导。
你可以通过遵循常见标准和惯例,并要求你的团队进行代码审查来提高代码的可读性。你可能会在开始时集中精力于代码的功能,但如果你在编写代码时使其可读,你会发现后续工作更简单。清晰有助于调试,如果其他人检查过并确认理解你的方法以及遵循一些基本规则,你会发现维护起来更容易。
在查看你的代码(或别人的代码)时,有几个提示:
可读代码的要素:Jo Stichbury(2023)公有领域
-
初学者常常使用缩写或短名称来命名函数和变量。如果你没有编写这些代码,很难理解它们;即使你编写了,也会发现几个月后很难理解。创建有意义的名称。
-
最好的代码是自我文档化的,意味着理解它所需的注释很少,但注释有助于在函数级别记录非平凡的代码。只要不要写出重复代码的大块文字即可。
-
通过使用空白来使代码可读。如果你使用 Python,你会发现这很简单,因为 Python 给空白赋予了语法意义。
-
编写只做一件事的小函数,具有单一返回路径和有限数量的参数。
-
不要使用硬编码的值;相反,使用精确命名的常量,并将它们全部放入一个配置文件中,以便你可以轻松找到和更新它们。像 OmegaConf、python-anyconfig 或 PyYAML 这样的配置管理工具旨在帮助实现这一点。
不要忘记文档
文档还可以帮助代码的可读性,并且在详细程度上有所不同:
-
基本内联注释
-
来自 docstrings 的 API 文档,解释如何使用/重用函数
-
Markdown 文件,如 GitHub 仓库根目录中的 README 页面,解释项目设置或特定使用细节。
保持你的文档最新,否则它可能会误导人,比没有文档更糟,并投入一些时间学习如何构建你的文档以发布它们,使用像 Jekyll 或 Sphinx 这样的工具。
4. 将笔记本代码重构为管道
到目前为止,这些建议已经足够通用,以至于初级软件工程师和数据科学家都能理解。这一点适用于处理数据摄取、转换、模型训练、评分和评估的顺序。
使用 Python 函数和包来形成管道可以对任务执行的顺序进行编码。有几个开源解决方案可以帮助构建这些类型的管道,例如 GNU Make,这是一个通用的遗留工具,仍然满足许多现代数据科学的需求,以及用于 Python 的 Snakemake。其他受欢迎的管道工具包括 Kedro、Luigi、Metaflow、Airflow、Prefect 和 Ploomber。选择工具时,你应该考虑学习曲线以及是否需要额外的功能,如能够调度管道运行。
管道的好处
可重复性:任何人都可以用很少的努力从原始数据中再现结果
正确性:结果是可以测试的
可读性:新的团队成员可以理解并掌握管道
扩展性:你可以将一个小的管道扩展为处理多个数据源、使用不同的模型和生成报告。
可维护性:你可以编辑和重新测试。正如我们之前描述的那样,Jupyter 笔记本非常适合快速原型,但它们就像你家门口的桌子或装满杂物的抽屉。无论你的意图多么良好,这里总会堆积杂乱的东西,如硬编码常量、打印语句调试和未使用的代码。笔记本中的代码越多,你就越难确定你编写的代码是否按预期工作。
测试,测试
使用管道可以将功能放入 Python 模块中,这样你可以测试、更新并再次测试,避免了“僵尸代码”对解释的干扰。pytest 框架 可以帮助你完成这些任务。
编写测试!如果你在“完成”的定义中包括编写测试(例如单元测试、集成测试和数据验证测试),你就不能跳过它们,并且在估算工作量时会将其计算在内。而且,良好的测试也可以作为文档,因为阅读测试可以帮助理解代码的功能。
在大多数数据科学项目中,大部分代码用于数据转换,而只有一小部分代码库是实际的机器学习。数据转换代码的管道可以进行测试(按定义,它们对相同输入应返回相同输出)。即使是机器学习代码也可以进行测试,以确认其是否按预期工作。你可以编写功能测试,以检查模型的指标(例如准确度、精确度等)是否超过预期阈值。
“尽早将代码从笔记本中移到 Python 模块和包中,以形成管道,以管理复杂性。”
5. 投资一些时间掌握版本控制
版本控制系统(VCS)如 Git、Mercurial 或 Subversion 允许你存储代码的检查点版本,以便你可以修改代码但在以后回滚到先前的版本。这就像拥有一系列备份,更重要的是,你可以与其他开发人员共享这些备份。
投资一些时间学习版本控制的原则,以最大化其带来的价值,这样你可以处理更复杂的场景,例如棘手的合并。还有一些优秀的实践材料可供学习,例如 Git 快速入门指南 和 学习 Git 分支教程。
在 YouTube 上 15 分钟学会 Git: youtu.be/USjZcfj8yxE
一些数据科学家学习了 commit
的基础知识,但这里有一些最佳实践需要考虑:
-
经常提交:如果你在大幅修改代码而没有提交更改,你可能会冒着失去已经花费时间的风险,因为不小心添加了一个破坏代码的更改,之后可能无法回滚。此外,喝咖啡时要小心放在笔记本电脑旁边!如果其他人也在进行代码修改,代码库可能会发生变化,而当你进行提交时,你可能会面临冲突和合并地狱。
-
只提交你需要的:如果文件是为了你的个人本地配置、秘密,例如数据库登录凭证或构建结果生成的中间文件,则不应存储所有文件。学习如何使用
.gitignore
。 -
不要在版本控制中存储原始数据。原始数据不会改变,所以你不需要对其进行版本控制。对于可以从原始数据和你的代码生成的中间数据文件也是如此。如果需要跟踪转化后的数据,可以使用不同的数据/工件/工作流版本控制工具,如 DVC(数据版本控制)或 Pachyderm,这些工具可以扩展你的 Git 代码版本控制。
使用版本控制在自己的项目和团队中有诸多好处,你可以大胆尝试任何代码中的风险,因为你可以迅速恢复到一个已知的、可用的状态。将稳固的版本控制系统流程与测试结合起来,提供了一种强大的工作方式。当你的代码产生正确结果时,保存它;然后在下一次更改代码时,重新运行测试。如果测试通过,说明你可能没有破坏任何东西。如果测试不通过,你可以重新工作或恢复。
摘要
自从哈佛商业评论 反映出对数据科学家的日益增长需求 已经过去了十多年:那些能够结合编程、分析和实验技能的人。虽然进入数据科学的职业道路仍然没有明确的定义,但现在有很多学习的途径,包括:
通常,这个角色吸引那些对数学有自信、乐于尝试复杂且凌乱的数据集并且能够编程的人。
数据科学现在可能已经确立了自己的角色,但软件开发已经有了几十年的成熟和经验积累。数据科学家可以学习的一些最有价值的技术是那些几代软件工程师建立的技术,例如结合版本控制、测试、可读的干净代码和良好的文件夹结构。这些可以使生产级项目成功与在原型阶段停滞之间产生差异。
如果你是寻求灵感的数据科学家,可以借鉴这些工程最佳实践以实现长期分析成功。
五件事 GenAI 能做和不能做的事情
原文:
towardsdatascience.com/five-things-genai-can-and-cant-do-d8117aad82f4
为商业领袖提供的关于生成型 AI 能做或不能做的介绍性指南
·发表于 数据科学的前沿 ·11 分钟阅读·2023 年 10 月 7 日
--
封面照片由作者创作
难以置信的是,自 ChatGPT 发布以来还不到一年,我们已经看到生成型 AI(GenAI)席卷了整个世界。从大型语言模型(LLM)到稳定扩散模型用于图像生成,这项新技术所能做的确实令人惊叹。一位朋友告诉我,这是 AI 第一次让他们感到触手可及,就像我们通过科幻小说梦想到的东西现在已经变成现实。
自然地,这让商业领袖们开始思考 GenAI 能或不能做什么来转变他们的业务流程。确实,你可以用 GenAI 做很多酷的事情,但也有一些流传的误解,商业领袖们应该小心。本文的重点是与您分享 GenAI 能够做的一些核心事情,同时也提醒对其不能做的事情保持适当的期望。
能做的 #1:GenAI 可以总结大量信息。
也许我听到的所有行业中最经典的用例之一是,特别是利用大型语言模型(LLM)将大量信息压缩成更易于消化的内容。例如,你可以将会议的转录对话交给 GenAI,总结成几个关键要点。此外,你可以将一份大型法律文件交给 LLM,让它提取出最相关的信息。当然,你应该始终小心核实 LLM 的输出是否正确,但这在许多不同的业务环境中可以节省大量时间。我高度预期这将在更多行业中持续获得关注。
不能做的 #1:GenAI 永远无法对任何事情有确切的确定性。
也许关于 LLM 的最大误解之一是它们能够思考。实际上,LLM 只是预测词汇的机器,尽管这些模型的精确度非常高,以至于看起来它们似乎在模拟真正的意识。由于 LLM 是基于词汇之间的概率进行操作的,它永远不能真正确定最终输出。然而,具有讽刺意味的是,它总是会产生非常自信的输出。我们将这些自信但错误的陈述称为虚构。
考虑以下句子:“I like to drink ______ in the morning。”如果你作为一个人来回答这个问题,可能会挠头。我们可以相当确定空白处是某种液体,但具体是什么液体呢?咖啡?茶?水?就像人类一样,LLM 也不能确定答案;然而,不同于人类,LLM 不会告诉你“我不知道”。相反,它会自信地给出一个答案,尽管你和我知道 LLM 不能真正确定答案。让我们实际看看 ChatGPT 如何尝试填补这个空白。
由作者拍摄的 iPad 上 ChatGPT 的截图
如你所见,ChatGPT 自信地将空白填为“coffee”,但它没有表明也许填错了。我们再次将这种过度自信的“猜测”称为虚构,但我们可以通过下一个要点来减少这些虚构……
#2:GenAI 可以通过增强的上下文提供更有根据的答案。
在前一点中,我们提到 LLM 对以下句子中的空白填充没有特别确定性:“I like to drink ______ in the morning。”然而,我们可以通过提供额外的上下文来增强 LLM 的知识。在 GenAI 社区,我们将此称为检索增强生成(RAG)。如果我们要求 LLM 在没有任何额外上下文的情况下填充上述空白,它肯定会给出一个答案,但那个答案可能是虚构的。现在,假设我们给 LLM 一些额外的上下文。让我们修改输入给 LLM 的内容如下:
我的名字是 David Hundley。我喜欢每天早晨喝一杯 Starbucks 的冷萃咖啡。使用这些信息,请填补以下句子中的空白:“I like to drink ______ in the morning。”
现在,LLM 已经获得了一些非常具体的上下文,它可以充分填补空白。在 ChatGPT 中测试时,你会看到我们收到的是精确的正确答案。
由作者拍摄的 iPad 上 ChatGPT 的截图
当然,这仍然不会使 LLM 的输出完全可靠。请记住,LLM 在评估词汇之间的概率。在 RAG 过程中,我们只是通过更正确的答案增强了输出的概率。这并不会将确定性的概率提升到 100%,但确实可以提供很大帮助!
无法解决 #2:GenAI 不能自己解决新问题。
尽管 RAG 过程可以在很大程度上帮助 GenAI 提供更精确的回答,但 RAG 过程并不是自动的。换句话说,像 LLMs 这样的 GenAI 技术没有自我学习的能力。这些 GenAI 模型是在某一时间点提供的信息上进行训练的,几乎就像它们的知识被冻结在那个固定日期。当我们向 LLM 提供额外的上下文时,这些上下文可以帮助生成更精确的输出,但这个 RAG 上下文实际上并不会改变基础模型本身。
我提到这一点的原因是,尽管 RAG 过程可能很有用,但你不能让 GenAI 自行去寻找这些信息。即使像 Bing Chat 这样的案例中,LLM 似乎在搜索互联网,但实际上并不是模型在爬取互联网。而是信息被带到 LLM,LLM 在理解这些带来的信息。 我们还没有达到那种像天网一样的超级智能水平,能够让这些 AI 模型自己解决问题。🤖
可以解决 #3:GenAI 可以是一个很棒的编码助手。
请特别注意这一点的措辞。我在这个点上对“能”这个词非常讲究。人们已经发现了利用 LLM 进行编码的方式,这并不是什么秘密。明确来说,LLM 在帮助编写代码方面可以非常出色。无论是自动填充常见任务还是帮助调试错误,GenAI 在编码时可以是一个非常有用的工具。
问题在于…… LLM 在这个任务上并不完美。除了受限于在某一时间点的训练信息外,根据我的个人经验,LLM 在被要求编写非常细致的代码时,往往会出现幻觉现象。请记住,LLM 仅能根据你提供的上下文来工作。 如果你有一个源于你非常特定系统的错误,LLM 无法了解你系统的细节,因此最终会产生幻觉性的答案。这并不是说 GenAI 无法在编写代码时提供帮助,但我认为它更像是一个助手。
无法解决 #3:GenAI 无法确定另一段内容是否由 GenAI 创建。
这是一个研究人员来回摇摆的点。实际上,OpenAI 曾经发布过一个工具,以帮助教师判断作业是否是使用 ChatGPT 创建的。最终,OpenAI 收回了这个工具,如果你了解这些 GenAI 解决方案的基础数学和架构,我认为我们很快将达到一个这种事情实际上是不可能的阶段。
考虑一下当前市场上最先进的 LLM,包括 OpenAI 的 GPT-4 和 Anthropic 的 Claude 2。这些 LLM 可以生成看起来非常人性化的输出,这是因为其基础架构能够以令人震惊的精度评估单词之间的概率。在实际与人工意识之间小心翼翼地踩线时,人们不禁想知道,是否也可以将人类语言预测为单词之间的概率。在那种情况下,LLM 和人类的最终结果在概率上是无法区分的,因此没有工具能够明确地说,“这是由 GenAI 制作的;这是由人类制作的。”
总之,不要被那些声称可以区分由 AI 生成的内容和非 AI 生成的内容的工具所迷惑。 也许在更原始的 GenAI 解决方案中,它可以进行一定程度的评估,但我们可以说,像 GPT-4 这样的 LLM 已经超越了这种区分的程度。
可以 #4:GenAI 可以通过其他软件过程获得帮助。
虽然大多数人更熟悉通过像 ChatGPT 这样的用户界面与 LLM 互动,但几乎所有主要的 GenAI 参与者都通过 API 等方式提供编程解决方案。这意味着我们可以将 GenAI 技术嵌入到新的和现有的软件过程中。事实上,大多数 GenAI 初创公司正在做的正是这一点。具体来说,许多 GenAI 初创公司使用像 OpenAI 或 Anthropic 这样的提供商的 API 作为后端“引擎”来支持他们的 GenAI 需求。(这也是为什么你应该小心你的公司选择与谁合作,因为他们可能在幕后与第四方互动!)
再次强调,GenAI 应该始终小心幻觉。虽然 GenAI 可以生成代码片段以执行如从数据库查询特定信息等任务,但我个人绝不会依赖 LLM 来完成这样的任务。因为我们不能百分之百确定代码的正确性,所以依赖 GenAI 是不明智的。这并不是说你应该完全不将 GenAI 结合到你的软件系统中!仍然有很好的用例,你可以以仍然有益且保持谨慎的方式在编程解决方案中利用 GenAI。
不能 #4:GenAI 无法正确引用其自身的信息来源。
这是我经常被问到的问题,如果你到现在为止都在认真听讲,你会明白为什么这是不可能的。尽管 LLM 是在某一时间点上的信息上进行训练的,但并不是说它将所有词语之间的概率联系到具体的来源。LLM 所存储的仅仅是被称为权重和偏差的东西。无需深入 LLM 的底层架构,LLM 本质上由大量数学操作组成,在训练时,这些权重和偏差会更新以更贴近它所训练的信息。(实际上,如果我详细解释 LLM 的架构,你会惊讶于数学的基础到几乎令人感到神奇的程度,LLM 能够做得如此之多!)
这就是 LLM 的全部内容。没有直接联系到源信息,因此如果你要求它引用自己的来源,你最多只能得到虚假的回答。我实际上相信像 ChatGPT 这样的特定 LLM 已经被微调,以表示不可能揭示其来源,我认为这比虚假的回答要好。但是,获得虚假回答并非不可能。有那个臭名昭著的案例,一名律师使用 LLM 在法律案件中引用了一个来源,结果发现该来源是虚假的,最终是假的。小心不要陷入同样的陷阱!
可以#5:GenAI 可以极大地增强你的知识。
与 GenAI 可以作为编写代码的良好助手的观点类似,GenAI 也可以成为任何知识任务的好助手。我个人将 LLM(如 ChatGPT)用作一种“搜索引擎”来回答一般知识问题。例如,它在提供词汇定义方面表现出色,并且在你继续提出额外问题时,它能给出更详尽的回答。就像和一个非常有知识的朋友互动一样。虽然互联网搜索引擎可以提供最新和最相关的信息,但对话的互动流程是基本搜索引擎无法比拟的。
当然,我不应该再提醒你注意幻觉。虽然大型语言模型(LLMs)可能在历史事件和地点如古罗马方面更为有用,但如果你问它今天的天气,它肯定会给出一个虚假的答案。如果它提供了正确的答案,那是因为 LLM 以 RAG 方式被增强,而不是因为 LLM 能自行得出这些信息。另外,特别要小心使用 LLM 解决数学问题。记住,这些 LLM 是通过寻找单词之间的概率来进行训练的,所以在面对数学问题时,并没有进行实际的数学计算。最近几个月,像 ChatGPT 这样的流行 LLM 似乎被增强以更好地处理数学问题,但不清楚它们是如何做到的。在任何情况下,我仍然建议尽量避免用 LLM 解决复杂的数学问题。
无法做到 #5:GenAI 无法取代你的工作。(…还没有?)
啊,是的,大家最关心的问题!其实,如果我完全诚实的话,GenAI 可以用来减少人力,但我们绝对还未达到它会取代大多数人工作的地步。如果你仔细阅读了本帖关于信息检索的部分,你会发现 GenAI 无法取代所有人的工作的最大“缺陷”:GenAI 无法主动自学。GenAI 无法与您的 CEO 交谈并了解他们脑中的想法。GenAI 无法进入你的脑袋,理解你在想象中构建的网站的所有细微差别。以这种方式,我认为 GenAI 就像一个完全天真的天才:你可以让它做很多事,但你必须直接而极其具体地与它沟通,以获得最理想的结果。
自然的后续问题是:AI 是否会达到能够完成这些事情的程度?我们将这一超级智能的下一个层次称为人工通用智能(AGI),这也是 AI 安全倡导者对 AI 进展表示关注的原因。目标是安全地使人类利益与 AI 利益对齐。不再深入讨论这一话题,重点是我们今天还未达到这一点,也真的不清楚我们何时会到达奇点——即我们达到 AGI 的那一点。如果你了解 AI 研究的历史,你会很快发现人类在预测这一点上并不擅长。 😂
我希望你能从这篇文章中更好地了解 GenAI 可能的优缺点。这是一项真正令人惊叹的技术,但并不是适用于所有情况。我还要强调的一点是:GenAI 并不是唯一的 AI。GenAI 的当前形式最早出现在 2017 年,但我们还有许多其他形式的 AI 也能有效完成任务。就像你不会租车走十英尺一样,你应该始终尝试调整解决方案以满足业务需求。GenAI 只是我们工具箱中的另一个工具,尽管是一个非常酷的工具! 😃
我从第一次 R 编程活动中学到的五件事
从 SatRDays London 学到的关于 R、数据科学和吸引观众的经验
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 5 月 11 日
--
图片由 Teemu Paananen 在 Unsplash 提供
上个月,我做了一件以前从未做过的事情。我参加了一个关于数据科学的线下活动——具体来说,是在 R 编程语言中进行数据科学。
我对会议并不陌生。作为心理学研究员,我参加并在许多相关领域的会议上做过报告。但尽管我是一个长期的 R 爱好者,我从未有机会参加关于数据分析的非学术会议。因此,当我有机会参加 SatRDays 伦敦,这个为期一天的专注于 R 的活动时,我毫不犹豫地报名参加了。
这是一个很棒的决定。我学到了很多关于 R 及其在许多行业中的应用的知识,其中一些是我以前从未听说过的。我还与许多对 R 像我一样痴迷的人交流,这真的很令人愉快。
这是我从 SatRDays 学到的五个最强大的东西之一。
1. R 的用途比我想象的要多
以为你知道 R 能做所有事情?再想想。
我有时听到数据科学家将 R 贬低为一个只有少数用途的专业语言。他们说,如果你不从事生物信息学、学术研究或硬核统计学,你所在的领域里没有人使用 R。Python 是一个通用的、“万能”的语言,你应该改用它。
在某种程度上,这是正确的——像 Python 这样的其他语言比 R 更广泛使用,并且在自身领域中也很有价值。但这并不意味着 R 不能在各个领域完成许多重要的任务。
SatRDays 的讲座范围令人难以置信。每位讲者来自不同的公司,展示了他们在行业中如何使用 R 进行数据分析的新颖而有趣的方法。
我听了数据记者、金融审计师、互联网性能分析师、空气质量专家以及更多人分享他们与 R 的经验。作为一个学术人员,这些领域中的一半在会议之前甚至不在我的雷达上。听到这些人的讲述让我意识到 R 在各种商业环境中的广泛用途。
当然,R 语言的工作机会没有 Python 语言那么多。但不要让任何人告诉你学习 R 是把自己逼入绝境。越来越多的公司开始采纳 R,并且有许多新的应用场景值得期待。
2. 良好的 R 代码将计算与操作分开
这是我从 Russ Hyde 关于良好编码实践的讲座中学到的一个提示。对于经验丰富的开发者来说,这可能是熟悉的,但对我来说却是新颖而有用的。
在编程时,通常最好将你的代码分成用户定义的函数。这有助于避免脚本中的重复,并确保代码是可重用和易于维护的。但,有些将代码拆分成不同函数的方法比其他方法更好。
比如,考虑一个简短的脚本,它读取一些数据,清理这些数据,然后将其写入一个新文件。这个脚本包含许多步骤,但我们可以将每个步骤分为两类:计算和操作。
read_csv("sales_data.csv") %>%
select(date, transaction_id, category, item_price) %>%
filter(date == "2023-05-10") %>%
group_by(category) %>%
summarise(day_turnover = sum(item_price)) %>%
write_csv("sales_summaries/day_turnover_2023-05-10.csv")
计算是一步步的操作,给定一定的输入,每次都会返回相同的输出。过滤数据集和计算摘要统计量等操作就是很好的例子。因为它们返回可预测的结果且没有任何副作用,所以很容易编写自动化测试来确认它们的正常工作。
对比起来,动作要难测试得多。它们会产生副作用,比如将数据写入文件,这些副作用更难控制。它们还可能受到全局环境中的随机数生成器(RNG)或其他变量的影响。这使得测试它们变成一项挑战,通常需要使用虚拟文件或受控环境。
提示是:尽可能地将动作与计算分离。
这里有一个使用我之前介绍的代码的例子。与其在一个块中包含动作和计算,不如将它们分成两个函数。所有读写数据的动作都包含在一个函数中,而所有的计算都在另一个函数中。
calculate_turnover <- function(sales_data, day) {
sales_data %>%
select(date, transaction_id, category, item_price) %>%
filter(date == day) %>%
group_by(category) %>%
summarise(day_turnover = sum(item_price))
}
action_turnover <- function(day) {
read_csv("sales_data.csv") %>%
calculate_turnover(day) %>%
write_csv(paste0("sales_summarise/day_turnover_", day, ".csv"))
}
action_turnover("2023-05-10")
这使得代码整洁而分隔明确。测试计算过程非常简单,因为它们与动作的任何副作用都被清晰分隔开来。这种分离也使我的动作更容易测试。
“哦,是的,他们在游戏开发中也经常这样做”,我向一位同事描述了这种方法后,他这样回答道。如果你有软件开发背景,你很可能已经知道这一点。但是数据行业的人员来自各行各业,有时可能从未学习过这些技巧。虽然我自学了一些良好的编码原则,但对我来说,这是一个新的领域,今后我会更多地加以利用。
3. 严格的测试是必不可少且被期望的。
谈到测试,这是整个会议的一个常见主题。
Vyara Apostolova 和 Laura Cole 在国家审计办公室的一次讲话中重点讨论了测试和检查。她们的工作涉及接收重要的政府模型,并在 R 中复现这些模型。在此过程中,她们会细致地测试每个模型中的每个假设,检查错误和差异。
这可能需要数年时间才能完成。但是,这种细致的工作对于发现昂贵的错误至关重要。她们揭示了,由于在金融模型中的错误导致的不必要的费用浪费,他们的工作已经节省了数百万英镑。这一切都是因为他们在严格的框架内测试、检查和评估他们编码的一切。
在其他讲话中,观众的问题经常与测试相关。如果有人展示了使用 R 进行某种新的酷炫方法,人们想知道的第一件事就是如何测试它。
尽管 R 在我们的学术教学和实践中是内置的,但测试却被高度低估。从外部视角来看,SatRDays 让我意识到,对于数据专业人士来说,拥有健壮的自动化检查工作流程是多么重要。
无论你是已经从事数据工作还是试图进入这个行业,测试都可以巩固你工作的价值。离开 SatRDays 后,我计划在几个月内找工作之前提高我的软件测试技能。
4. 学习 R 可能会很有趣。
我以前写过关于学习 R 的乐趣。但在 SatRDays 之前,我从未因数据科学演讲而大笑过。
如果我必须从整天的演讲中选择一个最喜欢的,那就是Andrew Collier的“整理世界的助手”。在这个演讲中,Andrew 介绍了一些不太为人知的 tidyverse 函数,以及它们如何补充更常用的函数。
在演讲中解释代码的工作原理是一个艰巨的任务。你需要在提供信息和避免让观众被技术细节困住之间找到平衡。SatRDays 的所有演讲都实现了这种平衡,但 Andrew 以突出的幽默和风格做到了这一点。
他的演讲充满了流行文化的引用和恰到好处的笑话。这些不仅有趣,还将新概念与观众已经知道的信息联系起来。
在演讲中使用幽默意味着让自己暴露在外。让观众笑一笑是一种特别温暖和令人满意的体验。当你的笑话在寂静的房间里回响时,那种感觉则完全相反。这就是演讲中的高潮和低谷。
话虽如此,幽默在技术演讲中是一个很好的工具,即使它并不总是有效。与其成为娱乐性的分心,它如果用得当,甚至能使你的解释更出色。往后,我会尝试在我的演讲中加入更多幽默,以使其更具特色。
5. R 社区独一无二。
R 社区非常棒。
这一点对于了解我人品的任何人来说都不应该感到惊讶。我鼓励在学习 R 时积极参与社区互动,甚至在选择使用哪些软件包时也是如此。
## Tidyverse 与 Base-R:如何为自己选择最佳框架
最受欢迎的 R 编程方法的优缺点。
towardsdatascience.com
但我从未与这么多 R 用户和专业人士待在同一空间里过。我在一天开始时不认识任何人,所以我希望能找到至少几个友好的人聊聊天。说实话,我有点紧张。
我不必担心。我遇到的每个人都很友好、平易近人且有趣。我不是一个外向的人,也不擅长社交,但我在所有休息时间都站着和人交谈。我知道这在所有事件中并不是必然的——我曾经有几次孤独的午餐没有伴侣,所以在这里尤其感到高兴。
R 社区也在多样化。Ella Kaye 和 Heather Turner 讨论了她们在让代表性不足的群体参与 R 语言维护方面的持续工作。目前,大多数主要的语言贡献者是接近职业生涯末期的西方男性。为了使 R 语言保持运作并与时俱进,重要的是将接力棒交给来自全球的更多样化的贡献者。Ella 和 Heather 分享了她们正在设置的各种倡议和活动,所有这些都听起来是前进的有希望的步骤。
我很高兴看到这种多样性在观众中得到了体现。虽然我算得上是众多戴眼镜的白人男性中的一员,但也有很多其他性别、特征和背景的人。我感觉每个人都被包括在内,组织者也积极促进了这一点。
如果这里有一个教训,那就是你不应该害怕与人建立联系,特别是在 R 社区中。他们可以为你提供关于工作、公司和技术的宝贵见解,这些都能推动你的职业发展。而且,至少在我的经历中,他们真的很友善。
参加一个 R 事件是一次很棒的经历,我希望以后能再去一次。我不仅了解了我最喜欢的编程语言,还认识了使用这门语言的人和相关行业。可以说,我将从 SatRDays 中获得很多收获,并在未来几个月内运用我新获得的知识。
如果你在阅读完后做些什么,请找到一些以你喜欢的数据科学技术为中心的活动,看看你是否可以去参加其中一个。组织 SatRDays 的公司 Jumping Rivers 有一个 R 事件列表供你查看。像RLadies这样的其他组织也经常在全球范围内举办面对面的聚会。
参加一个新的活动可能会让人感到紧张,但如果它与你及你的兴趣相关,通常是值得的。
想要将我所有关于 R 编程、数据科学等的文章直接送到你的邮箱吗? 点击这里订阅。
要获取我在 Medium 上的所有故事的完整访问权限,请通过此链接注册会员。
处理大型动作空间的五种方法
动作空间,尤其是在组合优化问题中,可能会变得庞大无比。本文讨论了处理这些问题的五种策略。
·发表于 Towards Data Science ·14 分钟阅读·2023 年 8 月 18 日
--
还有……行动![照片由 Jakob Owens 提供,来自 Unsplash]
处理大型动作空间仍然是强化学习中的一个相当开放的问题。研究人员在处理大型状态空间方面取得了重大进展,卷积网络和变压器是一些近期的高调例子。然而,存在三种所谓的维度诅咒:状态、结果和动作[1]。到目前为止,后者仍然是相对未被充分研究的。
尽管如此,处理大型动作空间的方法正在不断增长。本文介绍了处理后者的大规模的五种方法,特别关注在组合优化问题中经常遇到的高维离散动作空间。
回顾一下:维度灾难的三种诅咒
需要快速回顾维度灾难的三种诅咒。假设我们将手头的问题表达为一个 Bellman 方程 系统,请注意有 三组需要评估——实际操作中以嵌套循环的形式——每组可能都大得难以承受:
Bellman 方程要求对于每个状态-动作对 (s,a)∈S×A,必须评估所有潜在的结果 s’∈S’,这迅速使得对这个随机优化问题的枚举在计算上变得不可行。
从根本上讲,强化学习是一种蒙特卡罗模拟,通过采样随机过渡而不是枚举所有可能的结果。根据大数法则,样本结果最终应该促进收敛到真实值。通过这种方式,我们将随机问题转化为确定性问题:
值函数近似提供了一个确定性优化问题,在评估状态-动作对时,我们不需要评估整个结果空间,而只是一个单一的下游值。
这种转化使我们能够处理大的结果空间。要处理大的状态空间,我们必须能够对以前未见过的状态进行概括。常见的方法有特征提取或聚合,这也是研究的主要关注点。
由于我们可以评估一个对应于状态-动作对的单一值——而不是评估所有对应的结果——因此评估数百或数千个动作通常不是问题。对于许多问题(例如象棋、视频游戏),这就足够了,不需要进一步对动作空间进行近似。
尽管如此,如果动作空间仍然过大,我们不能使用与结果和状态空间相同的解决方案方法。仅仅对一个动作进行采样——就像我们对结果做的那样——并不能保证它是有效的,而且绕过了学习和使用智能决策策略的概念。像在状态上应用的概括方法也不起作用,因为我们最终需要一个可以应用到环境中的具体动作。因此,我们需要其他的解决方案。
什么是“大的”动作空间?
在深入解决方案之前,我们首先需要明确什么是“大的”动作空间。为了概括,我们用一些向量a=[a_n]_n∈N
表示动作,其中N
表示动作的维度。
“大的”这个术语可以用多种方式定义:
-
动作数量: 一个动作可以假设的值的数量。请注意,这个数量可能是无限的,例如整个整数域。显然,对于基于向量的动作,动作数量增加得比基于标量的动作快得多。考虑一个 9 维向量,其中每个元素可以取 10 个值。那么,总的动作空间已经达到 10⁹ = 10 亿个动作!
-
连续决策变量: 如果动作向量包含一个或多个连续变量,则动作的数量在定义上是无限的。连续变量在例如机器人学中很常见,其中关节运动由实值数表示。
-
维度:对于基于向量的决策,维度(元素数量)有巨大的影响——每增加一个元素,复杂性就呈指数级增加。特别是当动作是排列时,基于向量的决策空间会迅速变得非常大。
-
可枚举的:按照传统标准,一个有十亿个动作的动作空间可能被认为是‘大’的;你肯定不想在每次迭代时评估十亿个动作。不过,这样的动作空间仍然可以在计算机内存中被枚举和存储。相比之下,其他问题的动作空间巨大到我们根本无从枚举。如果动作空间随着问题规模的增长呈指数级增长,它们会迅速变得不可处理。
本文的重点将放在多维离散动作空间上。这些在现实问题中常常出现,导致组合优化问题的规模迅速膨胀。例如,考虑运筹学问题:管理出租车队、大量物品的库存补货政策、在船上堆放集装箱等。
让我们把我们面对的问题的规模变得更加具体。
考虑在一个港口管理集装箱运输,每个集装箱的特点包括(i)目的地,(ii)到期日期和(iii)到达日期。假设我们有 10 个潜在的目的地、20 个到期日期和 5 个到达日期。那么就有 10205 种独特的集装箱类型,这转化为一个 1000 维的动作向量。考虑到一些不同的运输选项和一个适度大的集装箱堆场,创建一个比地球上沙粒还多的动作空间并不难,或者任何你想做的类比。
另一个例子?
考虑一个包含 1000 个项目的推荐系统,我们向用户推荐两个项目。因此,有 1000*999(即约 100 万)种项目组合可以推荐。显然,推荐 3 个、4 个或 5 个项目会使动作空间呈指数级增长。
基于向量的动作空间通常会随着动作向量维度的增加而呈指数级增长。组合优化问题以生成非常大的动作空间而臭名昭著,即使对于看似简单的实例也是如此。[作者提供的图片]
I. 基于演员的方法(仅适用于连续动作空间!)
基于价值的方法,如 Q 学习,在连续动作情况下会崩溃,需要计算无限数量的 Q 值。可以对动作空间进行离散化,离散化的粒度决定了动作空间大小和准确性之间的权衡。例如,将方向盘的转动(连续空间)表示为 360 个离散度数,或者 36、3600 等。然而,对于基于向量的决策,这种离散化很快就会失效。
进入基于演员的方法[2]。这些方法通常使用神经网络,将状态作为输入,输出使从概率分布中采样的参数。例如,输出节点可以是高斯分布的均值和标准差,我们可以从中采样连续动作。
向量化决策的泛化相当简单,每个向量元素由一个单独的分布表示(即,相对于向量维度的线性缩放)。因此,也高维连续动作空间可以被高效处理。
关键在于演员网络直接将状态映射到动作,无需检查每个状态-动作对的值。如果需要——如为了减少方差——这些演员方法可以扩展为演员-评论员网络,其中评论员网络是一个扩展,输出生成的状态-动作对的值。对于连续动作空间,演员-评论员方法如 PPO 仍然是最先进的。
对于连续动作空间,演员网络高效地生成和评估动作,例如,通过输出高斯分布的均值和标准差来进行采样。扩展到多维动作向量时,统计参数对应于各个向量元素,从而使输出层的规模与动作维度线性相关。[image by author]
不幸的是,基于演员的模型在离散动作空间中扩展效果不佳。在这种情况下,每个输出节点代表单个动作的选择概率。显而易见,如果我们有数十亿个离散动作,这种方法不会很好地扩展。因此,对于离散动作空间,我们需要想出其他解决方案。
II. 数学编程
许多现实世界的决策问题可以通过凸动作空间来表达。凸性是一个非常强大的属性。你真的得亲眼见到才能相信。像 CPLEX 和 Gurobi 这样的解算器能够极其高效地处理大型决策问题,即使它们涉及到数千个决策变量,涉及到数百万甚至数十亿的动作[1]。
尤其是对于线性问题——许多实际问题(大致上)都是如此——解算器在过去几十年中已被高度优化。注意,分段线性函数可以用来近似非线性函数,例如,神经网络中的 ReLUs [3]。连续决策变量也没有问题。事实上,它们通常更容易处理。
数学程序也非常适合处理约束。执行显式可行性检查以构建动作掩码可能很繁琐。约束方程有效地筛选出所有缺失/不可行的动作。
尽管通过数学规划的规模提升可能非常显著,但对于不断增长的动作空间没有理论保证。计算工作仍可能随着动作空间的增大而呈指数增长。不过,在实践中,相较于优化,扩展性通常非常可观。
根据问题结构,数学规划的效果可能通过列生成或贝尔曼分解等方法得到增强。强大的实现可以将性能提升几个数量级。缺点是这些算法需要对问题结构有深入的了解,并且需要大量的设计工作。
大型和高维的动作空间通常是强化学习中的计算瓶颈。制定…
towardsdatascience.com
III. 启发式方法
启发式方法对动作空间施加搜索或决策规则,绕过对完整动作空间的评估,以提高速度和效率。
动作空间缩减
更高效地搜索动作空间的最简单方法之一是切割其部分区域。显然,这存在切割高质量区域的风险。然而,可以利用领域知识定义合理的决策规则,例如:
-
永远不要派遣填充率低于 70%的卡车
-
马里奥应该始终向右移动
-
始终建议在有人购买咖啡机时使用咖啡过滤器
可以看出,这很棘手。有时,马里奥可能需要向左移动以进行跳跃。也许我们需要等很久才能在安静的周内达到 70%的填充率。也许客户对咖啡豆研磨机而非过滤器更感兴趣。人类的专业知识、逻辑和直觉非常强大,但也有明显的缺点。
尽管启发式缩减可能显著降低计算负担,但其设计高度特定于问题,当问题实例发生变化时不一定具有可扩展性。
通过切割动作空间的区域(例如,添加约束),搜索可能变得不那么繁琐。然而,不恰当的切割可能会删除高质量的动作。[图片由Sdo提供,来源于维基百科]
元启发式方法
基础启发式方法通常依赖于人为定义的决策规则,这些规则可能是次优的。实际上,许多问题结构超出了人类的认知极限。为了自动化地寻找好的解决方案,元启发式(例如,模拟退火、遗传算法)指导动作空间的搜索,例如通过偶尔接受非改善的动作或重新组合它们[4]。
元启发式的搜索过程通常是与问题无关的,尽管不可避免地会有需要用户设计的特定于问题的元素。参数调整也是必要的——而我们在强化学习中已经有了大量这样的参数——同时,理论保证被放弃了(至少在实际应用中)。
尽管有缺点,元启发式方法在许多极具挑战性的优化问题中已证明了它们的实力,而可调搜索和启发式设计的强大组合确保它可以适应几乎任何动作空间。特别是对于混乱的现实世界问题,元启发式方法可能是处理大规模动作空间的最佳选择[1]。
数学启发式方法
一种特殊的启发式方法融合了 元启发式和数学编程 [5],旨在利用两者的优势。回顾一下,数学程序特别适合利用凸结构和大量决策变量,而启发式方法可以用来控制搜索空间和过程。如果动作空间如此之大,以至于即使是数学编程也不能在可接受的时间内返回解,将其与启发式方法结合可能是一个解决办法。
通用启发式算法实现包括如局部分支、邻近搜索和变量固定等技术。例如:我们可以启发式地减少一个动作空间,然后用数学编程搜索该动作空间。或者,我们可能用数学编程解决一个高层次的程序(例如,将客户分配给车辆),并通过启发式方法填充细节(例如,启发式生成路线)。有很多角度和不同的子程序分布。
数学启发式方法可能非常强大,规模远超纯数学编程——但牺牲了性能保证——但也可能设计上相当复杂。然而,当做到正确时,数学编程与启发式方法的结合可能是非常富有成效的。
IV. 连续到离散的映射
如前所述,基于演员的方法可以通过提供逐元素输出有效地处理多维连续动作空间。如果我们能对离散空间做到这一点就好了……
幸运的是,许多离散问题都有一些潜在的连续表示。例如:我们可能无法运输5.742…
个集装箱,但如果这样的实数解能给出好的解决方案,那么运输 5 或 6 个集装箱可能效果很好。
如果所需的连续表示存在,我们可以部署一个演员网络来获得所谓的原型动作(离散动作的连续近似),然后将其转换为“类似”的离散动作。这种从连续原型动作到离散动作的转换并不一定简单。是时候深入研究一些实现这一目标的映射了。
MinMax
将连续动作转换为离散动作的最直接方法是简单地四舍五入元素。例如,连续的原型动作 [3.67…,1.22…,2.49…]
可以转换为 [4,1,2]
。这样的映射可以稍微调整,但这就是一般的想法[6]。由于我们只需要采样一个连续动作并应用一个转换,这种方法可以扩展到极大的动作空间,超出枚举的范围。
如果连续量与其离散对应量之间有意义的结构关系,那么直接映射可能效果很好。例如,在库存管理中,将订购数量四舍五入到最近的整数是完全合理的。
不幸的是,并非所有离散动作空间都展示出如此干净的连续表示。假设我们训练一个推荐系统,整数表示要推荐的项目。如果 a=3
代表冰箱,而 a=4
代表搅拌机,最合适的邻居与原型动作 3.48…
相距远远不明显。让我们检查一些更先进的映射。
k-最近邻
k-最近邻(knn)方法也从一个原型动作开始,但随后在离散动作的邻域中搜索[7]。为了有效地做到这一点,整个动作空间被先验编码,这样我们可以快速找到我们可能定义的每个连续动作的 k
个最近邻。
在识别邻居后,knn 随后评估它们的 Q 值以选择最佳邻居。k
的值可以调整,更多的邻居可能识别更好的动作,但代价是更多的计算工作和越来越多的离策略动作。
knn 的优点和缺点都在于其对动作空间的先验枚举。预处理阶段使得邻居的高效查找成为可能,但也要求空间是可枚举的。
学习的动作表示
对于连续原型动作,“最接近”的离散邻居可能不是一个明显的选择,它们之间的关系在动作空间中可能有所不同。与其拥有固定的用户定义邻居,我们还可以学习将连续动作映射到离散动作的动作表示。在一个单独的监督学习阶段,神经网络被应用于学习每个离散动作的量身定制表示[8]。
学习的动作表示提供了强大的泛化能力,消除了对合适邻居的人工推理的需求,允许应用于复杂的动作空间结构。缺点是必须为每个离散动作学习动作表示,给 RL 算法增加了额外的复杂性。每个表示必须存储在内存中,因此该方法也限制在可枚举的动作空间中。此外,学习的动作表示可能在处理高维动作向量时遇到困难,因为目标向量变得越来越难以学习。
学习的动作表示尝试学习嵌入,将连续的原型动作映射到最合适的离散邻居,具体取决于动作空间的结构 [照片由 Michael Busch 提供,来自 Unsplash]
动态邻域搜索
MinMax 方法在其映射中简单直观,但最接近的邻居可能不总是最合适的。像 knn 和学习的动作表示这样的算法处理复杂的动作空间,但需要为每个动作提供显式的嵌入,因此只处理可枚举的动作空间。通过动态邻域构建 [9],离散邻域到原型动作被即时生成和评估,从而扩展到超出枚举的动作空间。遵循以下两个步骤:
-
扰动:通过扰动方案有效地生成邻域,每次改变一个元素,使得计算工作量与动作向量的维度成线性关系。
-
模拟退火:该扰动方案有效地生成离散动作的邻域,但省略了需要多元素扰动的动作。为了恢复这些动作(如果需要),模拟退火会探索新邻域,针对具有最高 Q 值的动作。
类似于 MinMax,动态邻域搜索需要一个特定的动作空间结构,其中小的扰动生成具有相似奖励的邻域。这种结构在许多实际问题中(例如,时空表示)可能存在,但该方法不能推广到所有离散动作空间。最后,每次迭代的运行时间较长,因为需要每次生成和评估动作,尽管它可能找到更优的动作。
V. 因式分解
因式分解(或称为分解)是一种方法,将动作分组并为每个分组找到动作表示,使得这些表示更易于学习。
因式分解方法的一个例子是二值化,其中所有行动都以二进制代码表示 [10,11]。对于每一位,学习一个相关的价值函数/子策略,指数性地减少评估次数。对于许多方法来说,完全枚举行动空间是先决条件。
二值化可以在超立方体上表示,我们可以对每一位进行结构化搜索 [图像来源:Vlad2i 通过 Wikimedia]
一种特殊的因式分解变体使用层次化或多智能体推理来因式分解行动空间 [12]。考虑管理一队出租车,其中每辆出租车的分配由单独的向量元素表示。对这支车队进行集中控制会产生大量的组合,这些组合都需要进行评估。
相反,我们可以在个体智能体的层面进行推理,在其局部环境中解决一个更简单的分配问题。鉴于上下文,这种多智能体视角可能是合理的,因为在城市另一边的出租车决策对当地决策的影响可能微乎其微。
尽管计算上比集中控制要容易得多,但得到的解决方案可能是次优的,例如,两辆出租车可能会接受同一个客户。为了确保行动的可行性并可能使用全局系统知识来改进它,需要一个事后同步步骤。
多智能体或层次化 RL 方法的设计可能具有挑战性,其适用性很大程度上取决于具体问题。然而,如果操作得当,分解可能会产生高质量的全局解决方案,同时在局部层面进行更快速的推理。
TL;DR
这篇文章相当长,因此如果你直接滑到最后,我不会怪你。这张表总结了最突出的要点。
对处理大行动空间的五种策略的简要描述,包括它们的优缺点 [图像来源:作者]
作者的其他强化学习文章:
从 REINFORCE 到连续控制中的首选算法的旅程
了解基于似然比的策略梯度算法(REINFORCE):直觉、推导、……
[towardsdatascience.com ## 强化学习的四种策略类别
[towardsdatascience.com
参考文献
[1] Powell, W. B. (2022). 强化学习与随机优化:序列决策的统一框架。John Wiley & Sons。
[2] Sutton, R. S., & Barto, A. G. (2018). 强化学习:导论。MIT Press。
[3] Van Heeswijk, W. & La Poutré, H. (2020 年 12 月). 线性离散动作空间中的深度强化学习。在 2020 IEEE 冬季模拟会议 (WSC) (第 1063–1074 页)。
[4] 维基百科贡献者 (2023). 元启发式算法。 en.wikipedia.org/wiki/Metaheuristic
[5] Fischetti, M., Fischetti, M. (2018). 数学启发式。在: Martí, R., Pardalos, P., Resende, M. (编) 启发式手册。Springer, Cham。
[6] Vanvuchelen, N., Moor, B. de & Boute R. (2022). 使用连续动作表示来扩展用于库存控制的深度强化学习。SSRN, 2022 年 10 月。dx.doi.org/10.2139/ssrn.4253600
。
[7] Dulac-Arnold 等 (2015). 大规模离散动作空间中的深度强化学习。arXiv 预印本 arXiv:1512.07679。
[8] Chandak, Y. 等 (2019). 强化学习的动作表示学习。在国际机器学习会议上,页码 941–950。PMLR, 2019。
[9] Akkerman, F., Luy, J., Van Heeswijk, W., & Schiffer, M. (2023). 通过动态邻域构建处理大规模离散动作空间。arXiv 预印本 arXiv:2305.19891。
[10] Dulac-Arnold 等 (2012). 快速强化学习与
使用误差校正输出代码的大动作集的 MDP 分解。在联合欧洲机器学习与数据库知识发现会议上, 180–194。
[11] Pazis J. & Parr, R. (2011). 大动作集的广义价值函数。在第 28 届国际机器学习大会 (ICML-11) 论文集中, 1185–1192。
[12] Enders, T., Harrison, J., Pavone, M., & Schiffer, M. (2023). 用于按需自主移动系统的混合多智能体深度强化学习。在 动态与控制学习会议 (第 1284–1296 页)。PMLR。
修复 Prophet 的预测问题
原文:
towardsdatascience.com/fixing-prophets-forecasting-issue-b473afe2cc70
第一步:限制疯狂的趋势
·发布于Towards Data Science ·10 分钟阅读·2023 年 1 月 24 日
--
图片由Hunter Haley提供,来源于Unsplash
目前,Prophet 预测准确性问题已不是秘密。它在多个基准和预测竞赛中一再交付糟糕的结果。然而,它仍然是最常用的预测算法之一……
所以…是时候通过一些小的调整来解决困扰它的问题,并(希望)提高它的预测准确性了。
预言者的趋势问题
奇怪的是,Prophet 的一个主要吸引力也是其核心弱点之一。它确实提供了一个引人注目的趋势,包含了变更点和线性段,分析起来非常易于消化。但是,有时,这种拟合趋势的方式既过度拟合又不足拟合——在序列末尾的一个简单水平偏移可能会让趋势在预测范围内无限延伸,同时在残差中留下大量有用信号。
这篇文章旨在解决第一个问题:不受限制的疯狂趋势。
为了说明这个问题,我们将使用 M4 数据集。这些数据集都是开源的,可以在 M-competitions 的github上找到。
首先,让我们看一下每周数据集,特别是这个数据集中第 52 个时间序列。
图片由作者提供
图片由作者提供
如果你之前使用过 Prophet,这些图表应该很熟悉。
在这里,我们对 100 的时间范围进行预测——远远超出了 M4 竞赛中对该系列的处理。但这展示了 Prophet 可能会如何失控,趋势从 7000 骤降至约 5500。这意味着时间序列在几年的过程中完全崩溃。从视觉上来看,我明白模型捕捉到的内容,并且在预测范围的近期,遵循这一点可能是可以的。然而,从长期来看,这种趋势可能会导致灾难性的结果。这个想法很重要,所以我会用粗体文字重新写一下:
短期内跟随趋势,长期内控制趋势。
在我们进入详细内容之前,让我们澄清一些术语。我见过‘dampen’、‘dampened’、‘dampening’和‘damped’这些词在不同来源和时代中交替使用。我知道有一个‘正确的’词,我曾经谷歌过,但我会根据自己的感觉使用它们。只要知道它们在本文中意义相同,如果你有偏好,务必告诉我!
现在开始修复问题。
修复趋势
通常,在时间序列背景下,‘damped’趋势出现时讨论的是双指数平滑。对我们来说,我们将采用更简单的方法,更符合‘经验法则’,而不是严格的统计方法。比如——指数衰减。
使用指数衰减,我们将从原始趋势非常接近开始,但在进入时间范围的过程中会逐渐远离它。同时,我们将接近一个‘目标’,该目标将作为一个连贯的参数提供。
让我们看一个简单的例子,首先是一个漂亮的曲线:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
sns.set_style('darkgrid')
y = np.linspace(0, 100, 100)
plt.plot(y)
plt.show()
作者提供的图片
真是漂亮的一条线!
接下来,我们将其分解为在处理时间序列时预期的不同部分。这里我们假设趋势等于实际信号,没有噪音或季节性。
y_train = y[:80]
future_y = y[80:]
future_trend = future_y
现在我们将使用‘future_trend’来抑制这个信号。为此,我们将安装一个名为‘TSUtilities’的包,它是我在其他包ThymeBoost、LazyProphet和TimeMurmur中使用的各种工具的集合,我希望将其集中管理。
pip install TSUtilities
仍在整理和结构化,但 TrendDampen 在这里,并可以像这样导入:
from TSUtilities.TSTrend.trend_dampen import TrendDampen
创建类时传递的两个参数是:
damp_factor:一个介于 0 和 1 之间的浮点数,其中 0 表示‘完全抑制’,1 表示‘不抑制’。
damp_style:如何‘抑制’趋势,通过平滑来‘平滑’地抑制趋势,使用指数衰减。
dampener = TrendDampen(damp_factor=.7,
damp_style='smooth')
dampened_trend = dampener.dampen(future_trend)
作者提供的图片
在这里我们使用 0.7 的 damp_factor,这意味着新趋势实现了旧趋势大约 70%的效果。我们在这里唯一需要传递给dampen
方法的是预测的趋势组件。
正如前面提到的,我们从接近原始趋势开始,但会越来越远。
现在这对我们帮助不大,因为我们传入了一个硬编码的 damp_factor
值,但我们可以利用这个方法中内置的一些进一步逻辑。如果我们传入拟合的组件:
-
训练实际值
-
拟合的趋势成分
-
拟合的季节性成分
(注:所有这些都是由 Prophet 提供的)
并将 damp_factor
改为‘auto’——方法将为你选择一个合适的参数。它通过评估拟合趋势的强度来实现这一点——这个趋势越强,我们就越愿意在长远中信任它。
注:这里的强度是通过 Wang, Smith, & Hyndman¹定义的趋势强度度量来确定的。
我们使用的这个‘经验法则’将根据算法的不同而有所不同。如果我们将逻辑应用于 ETS 方法,那么按照这些定义,趋势将会异常强烈,我们会过度信任它。但对于像 Prophet 这样的方法,或任何其他有某种确定性趋势的方法,它在实践中效果良好。
所以让我们看看这个方法如何处理我们简单的线。首先,我们需要一些拟合趋势和季节性的度量。
trend = y_train
seasonality = np.zeros(len(y_train))
那么我们将把这些值传递给 dampen
方法。
dampener = TrendDampen(damp_factor='auto',
damp_style='smooth')
dampened_trend = dampener.dampen(future_trend,
y=y_train,
trend_component=trend,
seasonality_component=seasonality)
plt.plot(future_trend, color='black', alpha=.5,label='Actual Trend', linestyle='dotted')
plt.plot(dampened_trend, label='Damped Trend', alpha=.7)
plt.legend()
plt.show()
图片由作者提供
正如你所见,‘auto’决定完全不抑制趋势!
这是一个很好的检查——虽然我相信你可以很容易地打破这个逻辑。
让我们看看这个序列的各种 damp_factors:
for damp_factor in [.1, .3, .5, .7, .9, 'auto']:
dampener = TrendDampen(damp_factor=damp_factor,
damp_style='smooth')
dampened_trend = dampener.dampen(future_trend,
y=y_train,
trend_component=trend,
seasonality_component=seasonality)
plt.plot(dampened_trend, label=damp_factor, alpha=.7)
plt.plot(future_trend, color='black', alpha=.5,label='Actual Trend', linestyle='dotted')
plt.legend()
plt.show()
图片由作者提供
很酷!
再次,我们有一个很好的、易于理解的参数,并且有一些‘auto’逻辑看起来还不错。
现在进入主要内容:修复 Prophet 的趋势。
在这个示例中,我使用了 M4 的每周数据集中第 52 个时间序列,并将预测 100 期。这个预测期虽然较长,但能够很好地展示问题。此外,我将使用一个函数,该函数接收 Prophet 的输出并抑制趋势,以返回这些修正后的预测。这个函数看起来像这样:
def dampen_prophet(y, fit_df, forecast_df):
"""
A function that takes in the forecasted dataframe output of Prophet and
constrains the trend based on it's percieved strength'
Parameters
----------
y : pd.Series
The single time series of actuals that are fitted with Prophet.
fit_df : pd.DataFrame
The Fitted DataFrame from Prophet.
forecast_df : pd.DataFrame
The future forecast dataframe from prophet which includes the predicted trend.
Returns
-------
forecasts_damped : np.array
The damped trend forecast.
"""
predictions = forecast_df.tail(len(forecast_df) - len(fit_df))
predicted_trend = predictions['trend'].values
trend_component = fit_df['trend'].values
if 'multiplicative_terms' in forecast_df.columns:
seasonality_component = fit_df['trend'].values * \
fit_df['multiplicative_terms'].values
dampener = TrendDampen(damp_factor='auto',
damp_style='smooth')
dampened_trend = dampener.dampen(predicted_trend,
y=y,
trend_component=trend_component,
seasonality_component=seasonality_component)
forecasts_damped = predictions['additive_terms'].values + \
dampened_trend + \
(dampened_trend * \
predictions['multiplicative_terms'].values)
else:
seasonality_component = fit_df['additive_terms'].values
dampener = TrendDampen(damp_factor='auto',
damp_style='smooth')
dampened_trend = dampener.dampen(predicted_trend,
y=y,
trend_component=trend_component,
seasonality_component=seasonality_component)
forecasts_damped = predictions['additive_terms'].values + dampened_trend
return forecasts_damped
但这个函数也在 TSUtilities 中,我们可以直接使用以下方法导入:
from TSUtilities.functions import dampen_prophet
现在让我们看看正常的 Prophet 输出和抑制后的输出有什么不同:
图片由作者提供
我们可以看到,Prophet 在预测范围内的最近趋势变化点远低于任何先前的值,而抑制后的趋势则显得更加保守。
从趋势的角度来看,我们可以看到究竟发生了什么,以及方法改变了什么。
图片由作者提供
注意,我们的方法并没有改变拟合的值。我们仅仅是旨在收敛未来的趋势成分。
最后,我们看到两种方法的 SMAPE——使用衰减趋势的准确性有了相当显著的提高:
图片由作者提供
在 M4 上的基准测试
到目前为止,我们只看了一个单一的例子,但现在让我们看看 M4 的两个数据集——每周和每日数据集。提醒一下,这些数据集都是开源的,并且作为还可以的基准,适合在时间序列领域进行尝试。
让我们继续导入每周的数据集:
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
import pandas as pd
from prophet import Prophet
import seaborn as sns
sns.set_style('darkgrid')
train_df = pd.read_csv(r'm4-weekly-train.csv')
test_df = pd.read_csv(r'm4-weekly-test.csv')
train_df.index = train_df['V1']
train_df = train_df.drop('V1', axis = 1)
test_df.index = test_df['V1']
test_df = test_df.drop('V1', axis = 1)
接下来,让我们定义用来评估预测的 SMAPE 函数:
def smape(A, F):
return 100/len(A) * np.sum(2 * np.abs(F - A) / (np.abs(A) + np.abs(F)))
现在我们已经定义了数据和度量指标,让我们遍历数据集,使用 Prophet 生成预测——一个标准预测和一个带有衰减趋势的预测。
seasonality = 52
no_damp_smapes = []
damp_smapes = []
naive_smape = []
j = tqdm(range(len(train_df)))
for row in j:
y = train_df.iloc[row, :].dropna()
y = y.iloc[-(3*seasonality):]
y_test = test_df.iloc[row, :].dropna()
#create a random datetime index to pass to Prophet
ds = pd.date_range(start='01-01-2000',
periods=len(y) + len(y_test),
freq='W')
ts = y.to_frame()
ts.columns = ['y']
ts['ds'] = ds[:len(y)]
j.set_description(f'{np.mean(no_damp_smapes)}, {np.mean(damp_smapes)}')
prophet = Prophet()
prophet.fit(ts)
fitted = prophet.predict()
# create a future data frame
future = prophet.make_future_dataframe(freq='W',periods=len(y_test))
forecast = prophet.predict(future)
#get predictions and required data inputs for auto-damping
predictions = forecast.tail(len(y_test))
predicted_trend = predictions['trend'].values
trend_component = fitted['trend'].values
seasonality_component = fitted['additive_terms'].values
forecasts_no_dampen = predictions['yhat'].values
forecasts_damped = dampen_prophet(y=y.values,
fit_df=fitted,
forecast_df=forecast)
#append smape for each method
no_damp_smapes.append(smape(y_test.values, forecasts_no_dampen))
damp_smapes.append(smape(y_test.values, forecasts_damped))
naive_smape.append(smape(y_test.values, np.tile(y.iloc[-1], len(y_test))))
print(f'Standard Prophet {np.mean(no_damp_smapes)}')
print(f'Damped Prophet {np.mean(damp_smapes)}')
print(f'Naive {np.mean(naive_smape)}')
结果如何?
图片由作者提供
衰减方法有效!但 SMAPE 仅减少了约 2.5%。虽然不足以大书特书,但还是一个不错的提升。值得记住的是,我们并没有真正失去 Prophet,只是改变了它的趋势。如果 Prophet 是你预测过程中的一个重要部分,那么这可以看作是锦上添花。
不过,对于每日数据集,结果确实有所改善。重新运行过程得到:
图片由作者提供
瞧,SMAPE 减少了 7.8%!
与其他在 M4 上进行基准测试的模型比较这些结果时,我们发现即使有这些改进,Prophet 仍然无论如何都产生较差的结果。我想一个理由是我们没有调整超参数。不管怎样——我不会使用 Prophet。
我不会在集成模型中使用 Prophet。
我不会使用 Prophet 进行多变量预测。
我绝对不会输入:import Prophet
但这一系列的重点是尝试增强Prophet。下次——如果有下次的话——我们会有另一个技巧来实现这个目标。
结论
我们展示了基于经验法则的趋势衰减过程在提高 Prophet 预测准确性方面的有效性。该过程非常灵活,可以与任何输出一起使用(尽管 dampen_prophet 函数本身不灵活)。我们在 M4 系列的两个数据集上观察到 SMAPE 减少了约 2.4%和约 7.8%。
如果你喜欢这篇文章,你可能会喜欢我其他的一些文章!
## LazyProphet: Time Series Forecasting with LightGBM
一切都与特征有关
[towardsdatascience.com ## Time Series Forecasting with ThymeBoost
一种梯度提升时间序列分解方法
towardsdatascience.com ## 梯度增强 ARIMA 用于时间序列预测
提升 PmdArima 的 Auto-Arima 性能
towardsdatascience.com
剧透警告
本系列的终点将完全摆脱 Prophet —— 思路类似于 Nixtla 使用的 StatForecast 包。与 Nixtla 不同的是,我们将保留相同的时间序列分解和易于理解的线性变点趋势。这将通过 ThymeBoost 实现。
当然,您也可以通过我的推荐链接注册 Medium:
[## 通过我的推荐链接加入 Medium - Tyler Blume
阅读 Tyler Blume 的每一个故事(以及 Medium 上成千上万其他作家的故事)。您的会员费将直接支持……
medium.com](https://medium.com/@tylerblume/membership?source=post_page-----b473afe2cc70--------------------------------)
参考文献
- Wang, X., Smith, K. A., & Hyndman, R. J. (2006). 基于特征的时间序列数据聚类。数据挖掘与知识发现, 13(3), 335–364.
Flapjax: 使用 Plotly 和 Flask 进行网络数据可视化
原文:
towardsdatascience.com/flapjax-data-visualization-on-the-web-with-plotly-and-flask-465090fa3fba
使用 Plotly 和 Flask 构建一个数据可视化网页,并用一些 UI 组件使其互动
·发表于 Towards Data Science ·17 分钟阅读·2023 年 11 月 17 日
--
构建数据可视化应用程序的最佳框架是什么?是 Streamlit 还是 Dash?或者你可以用 Mercury 或 Voilá 将 Jupyter Notebook 转换为网络应用程序?
所有这些都是创建应用程序的好方法,而且相对容易入门。但通常,容易入门的东西会随着你变得更有冒险精神而变得稍微复杂一些。因此,我将试图说服你,回到基础,使用 Python 服务器代码和 HTML 页面作为用户界面,其实并没有看起来那么令人生畏。
我们可以使用相当多的模板代码和模板构建引人入胜的交互式应用程序,这意味着你可以仍然集中精力在你的 Python 代码上,对 HTML 和 Javascript 的接触是最小的。我称这种方法为Flapjax——稍后我会解释原因。
创建一个简单的 Python 网络应用程序的一种最简单方法是使用 Flask,这正是我们要做的,我们将创建一个看起来像下面图片中的应用程序。
一个示例互动应用程序
Flask 框架
Flask 是一个用于开发 Web 应用的极简框架。在 Flask 应用中,网页通常是由模板和 Python 代码提供的数据构建的——这些数据可以是形成网页内容的文本或图形。结果会被发送给用户以在浏览器中显示。
下图展示了一个交互式应用的基本结构。当应用运行时,Python 部分在服务器上执行,并将数据传递给在浏览器中运行的 HTML。网页上的用户输入会传回 Python 代码,Python 代码可能会发送更多数据以更新 HTML 内容,例如用户选择的新图表。
这是创建 Web 应用程序的最简单方法吗?
在我看来,将用户界面设计与程序逻辑分离确实使生活变得更轻松。但如果你习惯于在 Streamlit 或 Jupyter Notebooks 中构建应用,你可能会发现有一定的学习曲线。然而,一旦你采用了一个基本应用的模式,创建新的应用会容易得多。
因此,我们将使用 Flask 开发一个数据可视化应用,并且我们还将使用 Jinja 模板来定义我们的 HTML 页面——虽然实际出现在这些页面中的数据将由我们的 Python 代码定义。
要制作一个交互式用户界面,我们需要一些 UI 组件和一点 JavaScript,但我们会看到这基本上是可以在未来应用中重用的样板代码。
我们还将使用 Bootstrap 5 UI 组件,因为你为什么要满足于一个看起来像左侧的网页呢,而稍加努力就可以让它像右侧的那个网页一样?
本教程分为两个部分:首先,我们创建一个静态网页,了解 Flask 和 HTML 如何协同工作;接下来,我们将处理回调,以创建一个交互式页面。
本文的所有代码将存储在我的 GitHub 仓库中。我会在文章发布后的不久提供链接。
Bootstrap UI
使用 Bootstrap 创建吸引人的网页并不需要太多努力。包含 Bootstrap 5 文件并为 HTML 元素添加一些属性,可以轻松改善基本 HTML。
这不是一个 Bootstrap 教程,但让我快速向你展示上面两个网页中标题的基本 HTML 代码之间的区别。
基础 HTML
<header>
<h1>Title</h1>
<p>subtitle</p>
</header>
添加了 Bootstrap 属性
<header class="bg-primary text-white text-center py-2">
<h1 class="display-4">Title</h1>
<p class="lead">subtitle</p>
</header>
你可以看到,头部由两个元素组成,一个 <h1>
,即顶级标题,以及一个段落 <p>
。在 Bootstrap 版本中,这些元素有了额外的属性:头部本身具有 primary 背景色和白色文本,文本居中,上下边距设置为 2 像素;标题标签使用 display-4
字体,而段落的字体设置为 lead
—— 这些字体在 Bootstrap 中定义,其中 display
字体大而粗体,而 lead
字体用于需要突出的普通文本。
这些特性是在 HTML class
属性中设置的。我们将在接下来的代码中看到更多这些特性,它们应该相当容易理解。我不会详细描述这些特性,但你可以在 他们的网站 上找到 Bootstrap 文档 —— 这将告诉你所需了解的一切。
一个 Flask 项目
Flask 框架使得创建基于 web 的应用程序变得简单。一个 Flask 应用通常由至少两个文件组成:一个 Python 应用程序和一个 HTML 模板。
Python 部分包含应用逻辑:例如,在数据可视化应用中,它可能将数据加载到 Pandas 数据框中,进行一些分析,并在 Plotly 中创建图表。HTML 模板定义了网页的布局,并由 Python 程序提供显示的数据。
/project_name
|--- app.py
|
|--- /templates
|
|--- index.html
一个简单应用的目录结构,如我们将要创建的那样,应该类似于上面的示意图。主 Python 应用程序位于项目文件夹中,模板文件位于名为 templates 的子文件夹中。
当然,你还需要安装 Flask:
pip install flask
在这样做之前,你可能需要创建一个虚拟环境。
Flask 应用的 Python 部分定义了一个或多个应用将响应的路由。通常其中一个路由是‘/’,即项目的根目录。
下面是一个使用模板的最小 Python 应用。模板名为 index.html,必须位于 templates 文件夹中,并由 Flask 使用 render_template()
库函数呈现为网页。请注意,我们为网页标题创建了一个值并将其传递给函数。
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
title = "This is the title"
return render_template('index.html', title=title)
下面是 index.html 模板,它期望 title
值被纳入其中。你可以看到,在 <h1>
标签中,标识符 title
被包围在双大括号中。
<!DOCTYPE html>
<html>
<body>
<h1>{{title}}</h1>
</body>
</html>
Flask 使用 Jinja 模板引擎来用传递给 render_template()
的值替换 HTML 模板中的占位符。
你可以通过在终端中输入 flask run
来运行应用程序,你应该会得到类似于下面的响应。(这假设你将应用程序命名为 app.py —— 如果你用其他名字命名,需要输入 flask --app app_name run
。将 app_name
更改为你的应用程序名称,不包含 .py
扩展名。)
flask run
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
将浏览器指向 http://127.0.0.1:5000 或 localhost:5000,你将看到一个简单的网页,展示了在 Python 代码中定义的文本。
想要了解更多关于 Flask 的信息,你可以从查看他们的快速入门教程开始,但我希望我会在这里涵盖你开始所需的所有内容。
一个静态数据可视化应用程序
我们的第一个应用程序将基于迄今为止看到的内容,创建一个包含 Plotly 图表和一些辅助文本的网站。稍后,我们将添加一些交互功能。
让我们看看 Python 的部分。下面的代码列表中,请暂时关注以#### Simple template ####
开头的部分。在这里,你可以看到我们定义了一个名为/simple
的路由,这意味着我们通过将浏览器指向localhost:5000/simple来调用应用程序,而装饰器下方的simpleindex(),
函数将会被执行。
在这个函数中,我们设置了一些文本和我们希望在网页上显示的图形。我们首先设置了一些变量,然后使用这些变量创建一个 HTML 模板将使用的参数字典。变量的名称清楚地表明了它们的使用方式。
get_graph()
函数设置了图形参数。首先,它加载了 1881 年至 2022 年的全球温度异常数据,并追踪气候变化如何影响这段时间的温度(有关详细信息,请参见新数据表明 2023 年是有记录以来最热的夏天)。这些数据在下表中显示(在图表中会更清晰!)。
全球温度异常
对于静态应用程序,我们将使用单列JJA
,它指的是 6 月、7 月和 8 月的温度。交互式应用程序也会使用其他一些列,因此我们当前应用程序的period
参数有一个默认值,之后可以被交互式应用程序更改。
数据用于创建一个 Plotly 条形图,生成的图形被转换为 JSON,网页将使用这些数据。因此,这些 JSON 数据被返回以设置字典参数中的graph
条目。
回到之前的函数,我们现在需要使用我们设置的参数调用render_template
。为了节省输入时间,我创建了一个名为template
的辅助函数,它提取模板参数并将所有数据传递到网页上。
from flask import Flask, request, jsonify, render_template
import json
import pandas as pd
import plotly.express as px
app = Flask(__name__)
def get_graph(period = 'JJA'):
df = pd.read_csv('GlobalTemps1880-2022.csv')
fig = px.bar(df, x='Year', y = period,
color=period, title = period,
color_continuous_scale='reds',
template='plotly_white', width=1000, height=500)
graphJSON = fig.to_json()
return json.dumps(graphJSON)
def template(params):
return render_template(params['template'], params=params)
#### App ####
@app.route('/')
def index():
return render_template('index.html')
#### Simple template ####
@app.route('/simple')
def simpleindex():
header = "Global Temperature"
subheader = "Global Temperature changes over the last few centuries"
description = """The graph shows the increase in temperature year on year.
The data spans the years 1881 to 2022 and includes temperature anomalies
for the months June through August.
"""
params = {
'template':'simpleindex.html',
'title' : header,
'subtitle': subheader,
'content' : description,
'graph' : get_graph()
}
return template(params)
#### Main ####
if __name__ == '__main__':
app.run(debug=True)
现在来看 HTML 模板。下面的代码列表中,好消息是你可以忽略除<body>...</body>
标签内的代码外的所有内容。其余的都是你需要的 Bootstrap 和 Plotly 网页的样板代码,可以剪切并粘贴到任何类似的网页中。最后的<script>
标签也是样板代码,包含了 Bootstrap 的 Javascript 代码,可以安全忽略。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
</head>
<body>
<header class="bg-primary text-white text-center py-4">
<h1 class="display-4">{{ params.title }}</h1>
<p class="lead">{{ params.subtitle }}</p>
</header>
<div id = 'content' class="container mt-4">
<div id='chart'></div>
<div class="lead">{{params.content}}</div>
</div>
<script type='text/javascript'>
var figure = JSON.parse({{params.graph | safe}})
Plotly.newPlot('chart', figure, {});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
如果我们忽略使页面看起来漂亮的 Bootstrap 属性,我们得到的代码如下,这要简单得多,这就是我从这里开始引用的内容。
<header>
<h1>{{ params.title }}</h1>
<p>{{ params.subtitle }}</p>
</header>
<div">
<div id='chart'></div>
<div>{{params.content}}</div>
</div>
<script type='text/javascript'>
var figure = JSON.parse({{params.graph | safe}})
Plotly.newPlot('chart', figure, {});
</script>
我们之前见过使用 Jinja 参数,这次唯一的不同是我们将几个参数打包成一个名为 params
的字典。因此,我们通过在参数名称前加上字典的名称来引用它们。因此,<h1>{{params.title}}</h1>
只是将 title
参数放入一对标题标签中。其他三个文本参数的标签也是类似的。
为了绘制图表,我们需要一个可以放置图表的元素,并且该元素必须有一个 id 属性 (<div id='chart'></div>
)。这个元素被放置在标题下方和描述上方。
下面的脚本元素再次是调用 Plotly Javascript 绘制图表的样板代码。唯一需要注意的是,当我们包含 graph
参数时,我们使用了 safe
关键字。这指示 Jinja 不要尝试解释 graph
中的任何特殊字符,而是将它们按字面意思对待。因此,代码如下:
var figure = JSON.parse({{params.graph | safe}})
现在,请记住模板必须在项目目录中的 templates 文件夹中,并且为了使此代码正常工作,数据文件必须位于项目目录本身(当然,你可以移动它,但在 Python 程序中打开时必须更改路径)。
所以运行应用程序并在浏览器中指向 localhost:5000/simple,你应该能看到一个如下图所示的网页。
一个静态应用
这就是网页应用的静态版本。
一个互动数据可视化应用
但是为什么要限制在夏季?如果我们能够选择除 JJA 以外的其他时期不是很好吗?数据还包含全年的列,J-D,以及三个时间段:DJF、MAM、JJA 和 SON。(这些字母代表英文中的月份名称:十二月、一月、二月;三月、四月、五月;等等。)
为此,我们需要整合一个用户控件,用于选择适当的时间段。我选择了一个下拉菜单,用于显示各种时间段。它将与之前的网页非常相似(参见下图)。
一个互动应用
代码最初也非常相似。主要区别出现在处理新图表的选择时。
当选择一个新值时,这将调用一个 Javascript 函数,该函数将值发送到服务器上的回调函数并等待响应。这个回调函数将返回一个新的图表,然后由调用的 Javascript 显示。
让我们先处理熟悉的内容。下面是实现新端点 /ddsimple 的函数。
@app.route('/ddsimple')
def ddsimpleindex():
# The root endpoint builds the page
header = "Global Temperature"
subheader = "Global Temperature changes over the last few centuries"
description = """The graph shows the increase in temperature year on year.
The data spans the years 1881 to 2022 and includes temperature anomalies for periods of each year as indicated.
"""
menu_label = "Select a period"
params = {
'template': 'ddsimpleindex.html',
'title': header,
'subtitle': subheader,
'content' : description,
'menu_label': menu_label,
'options' : [{'code':'J-D', 'desc':'Whole year'},
{'code':'DJF','desc':'Winter (North)'},
{'code':'MAM','desc':'Spring (North)'},
{'code':'JJA','desc':'Summer (North)'},
{'code':'SON','desc':'Autumn/Fall (North)'}],
'graph' : get_graph()
}
return template(params)
如你所见,它与/simple端点非常相似。区别(除了名称)在于额外的参数:菜单的标签和一个表示菜单项列表的字典,首先是一个对应于数据框列的值,其次是该值的文本描述,这些描述将显示在菜单中。
HTML 稍有不同,因为它展示了 Jinja 的更复杂用法。代码如下。
<form id="userForm" name="form1" onChange="getFormValues('form1')">
<div class="mb-3">
<label for="dropdown" class="form-label lead">{{params.menu_label}}</label>
<select class="form-select" id="dropdown" name="dropdown">
{% for opt in params.options %}
<option value="{{opt.code}}">{{opt.desc}}</option>
{% endfor %}
</select>
</div>
</form>
<div>
<!-- Main Content Area -->
<p class="lead">{{params.content}}</p>
</div>
<div id="graph"></div>
在这里,我们构建了一个包含下拉菜单的表单。该表单还包含一个菜单标签,这个标签包含在我们之前见过的双花括号中。
与之前的示例相比,主要区别在于菜单的构建方式。在<select>
元素内,我们需要放置一系列代表菜单选项的<option>
标签。<option>
标签具有一个值和一个描述,这些值和描述是在params.options
字典中定义的。我们通过执行类似{% for opt in param.options %}
的 Jinja 循环来包含这些值和描述,该循环遍历字典,将每个元素放入本地变量opt
中。然后,我们利用这些值和描述,通过使用opt.code
和opt.desc
将它们插入到<option>
标签中。
你可以在Flask 教程:Pythonbasics.org 网站上的模板中找到 Jinja 模板的简单示例和解释。
表单标签中还有另一个对我们目的至关重要的部分。表单标签内有一个名为onChange
的属性,它接受某种类型的动作值,在这种情况下,它是一个 Javascript 函数,每当表单中的值发生变化时——在本例中,当选择菜单中的选项时,就会调用这个函数。
这就是有趣的开始。
回调
为了用新图表更新网页,我们使用回调机制,下面的图表展示了浏览器与服务器之间的事务。请注意,响应请求新图表的方式是不是重新加载页面,而是更新页面——这种方式更快,并且避免了重新加载时出现的瞬时空白屏幕,从而提供了更好的用户体验。
使用回调更新网页
回调是通过网页上表单的变化来调用的,正如我们之前提到的。这种机制是一个在表单的onChange
属性中标识的 Javascript 函数。
我将详细解释 Javascript 函数的工作原理,但基本上,它从表单中获取值,并将这些值发送到 Flask 应用中的回调端点。
现在,如果想到编写 Javascript 让你感到不安,不用担心,你不需要真正了解这些内容,你可以直接复制它,它适用于你可能想在网页上包含的任何表单。因此,对冒险者而言,解释如下,对于其他人——直接跳到下一部分。
实际上有两个函数:从表单中获取值的函数如下所示。
function getFormValues(f) {
const form = document.forms.namedItem(f);
const formData = new FormData(form);
const value = Object.fromEntries(formData.entries());
postJSON(value);
}
所有代码都使用内置的 Javascript 函数:第一行从文档(即网页)中获取表单;第二行检索包含表单数据的数据结构;第三行提取该结构中的所有值。
最后,这些值被传递给另一个函数 postJson
。
async function postJSON(data) {
try {
const response = await fetch("/callback", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("Success:");//, result);
drawGraph(result);
}
catch (error) {
console.error("Error:", error);
}
}
这个函数是一个异步函数,这意味着在调用后,程序执行会立即返回到调用代码,而异步函数会在一个单独的执行线程中继续运行。也就是说,它会与网页的执行并行地继续执行所需的操作。
postJSON
函数接收需要发送到 Python 回调代码的数据,并使用异步 fetch
函数发送。fetch
接收数据应该传递到的端点和数据本身作为参数——我们使用 HTTP POST 机制来发送数据。postJSON
等待 fetch 完成,即等待服务器返回一些数据。然后将这些数据传递给 drawGraph
函数,更新网页上的图表。
请注意,代码被包含在 try... catch...
块中。这与在 Python 程序中看到的基本相同:如果 try
块中的代码失败——没有响应或其他通信故障——那么该故障将被记录到控制台中。
Python 回调
为了使这一切工作,我们需要一个 Flask 代码中的回调函数,该函数将接收数据,对其进行处理(即创建新图表),并返回结果。如下所示:
@app.route('/callback', methods=['POST'])
def callback():
# The callback updates the page
if request.is_json:
data = request.get_json()
return get_graph(period=data['dropdown'])
else:
return jsonify({"error": "Invalid JSON data"}), 400
首先需要定义回调的端点,你可以看到我们还指定了端点期望使用 POST 方法发送数据。
我们也期望数据为 JSON 格式,如果不是,则返回错误。
如果数据是 JSON,我们从下拉菜单中提取值,并将其传递给get_graph
函数,该函数绘制图表并以 Plotly 期望的 JSON 格式返回图表数据。这个图表数据由网页上的 Javascript 函数接收,页面得到更新。
这段代码将为你提供上面显示的交互式网页。
Flapjax —— 名字中有什么
诚然,这个名字有些刻意:它代表了Flask,Python,Javascript 和 ax,这些代表了使这种技术得以实现的异步通信。
我希望你能看到,通过使用预先编写的模板和一段模板代码,你可以创建有用的交互式网页,同时主要集中于 Python 代码的逻辑。
这个应用程序仅包含一个图表,当用户选择新选项时,该图表会更新,但可以通过这种方式从 HTML 表单中收集任意数量的值,这些值可以由 Flask 应用程序处理,然后用新信息更新网页(也许我们会在未来的文章中探讨这个问题)。
所有在这里展示的代码和数据可以从我的 GitHub 仓库 下载(查看 jinja-article 文件夹)。
更新:我在 GitHub 仓库的 reuse 文件夹中编写了一个新应用程序,该应用程序使用一组新的数据——HTML 和 Javascript 保持不变,只有 Python 代码和数据发生了变化: 重用 Flapjax 模板和代码
我希望你觉得这些内容有用。如果你想查看更多我的作品,请访问我的 网站,并通过订阅我的免费通讯来获取我发布的更新,点击这里:
[## 数据可视化、数据科学和 Python | Alan Jones | Substack
关于数据科学、数据可视化及主要使用 Python 进行的动手编码的教程和其他文章。点击…
如果 Flask 不是你的首选,我根据我在 Medium 上的文章编写了一本电子书 从头开始学习 Streamlit。
说明和参考文献
本文和应用程序中使用的数据来源于下面第 1 和第 2 条说明中的描述。
-
GISTEMP 团队,2023: GISS 地表温度分析 (GISTEMP),第 4 版。NASA 戈达德空间研究所。数据集访问日期 2023–09–19,网址:data.giss.nasa.gov/gistemp/。请注意,NASA 数据集的使用没有特定的许可证。NASA 将这些数据集免费提供用于非商业目的,但应给予归属(如上所述)。
-
Lenssen, N., G. Schmidt, J. Hansen, M. Menne, A. Persin, R. Ruedy, 和 D. Zyss, 2019: GISTEMP 不确定性模型的改进。J. Geophys. Res. Atmos., 124, no. 12, 6307–6326, doi:10.1029/2018JD029522。
除非另有说明,所有的图像、图表、截图和代码均由作者创建。
关注 TDS 列表,发现我们的最佳文章
一种方便的方式来跟进我们作者的最强和最受欢迎的作品
·
关注 发表在 Towards Data Science ·2 分钟阅读·2023 年 7 月 18 日
--
照片由Alisa Anton拍摄,发布在Unsplash
我们每周发布数十篇精心制作的文章,主题涵盖最新的机器学习工具和 Python 库到前沿研究。现实中,阅读我们分享的每一篇文章非常困难,但这并不意味着你应该错过你最有可能喜欢的那些文章。
为了帮助你跟上数据科学、人工智能和机器学习的最新动态,我们创建了一些Medium 列表,在这些列表中汇集了我们推荐的阅读内容。我们将定期更新每个列表,因此你只需保存一次,就可以轻松访问你 Medium 图书馆的收藏列表标签。
(不确定如何保存列表?只需点击以下任意列表上的查看列表按钮,然后点击列表标题下的书签符号。)
寻找 TDS 的最佳内容?我们创建了数据科学和机器学习必读列表,让你随时能接触到我们各个话题和格式的最强文章。
数据科学和机器学习必读
查看列表275 篇故事
如果你喜欢较长、深思熟虑的文章,我们的数据科学和机器学习深度解析列表就是你最好的去处——从教程到解释,这些帖子因其深入和易懂而脱颖而出。
数据科学和机器学习深度解析
查看列表248 篇故事
坚信群体智慧的人应该收藏TDS 热门列表,我们将在此突出展示流行和广泛传播的故事。
TDS 热门
查看列表197 篇故事
我们希望您喜欢使用我们的新列表,并且这些列表能激发您去探索那些您之前没有关注过的作者的作品。如果您有其他您认为有用的 TDS 列表的想法,请留言告知我们。
祝您阅读愉快!
遵循此数据验证过程以提高数据科学准确性
当训练数据和推断数据来自不同来源时
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 9 月 1 日
--
图片由 NordWood Themes 提供,出处:Unsplash [1]。
目录
-
介绍
-
启用数据收集
-
设置基准
-
检测异常值
-
摘要
-
参考资料
介绍
本文旨在为数据科学家提供数据验证过程的指南,特别是那些刚开始或希望改进现有数据验证流程的人,提供了一些示例作为一般性概述。首先,我想在这里定义数据验证,因为它对其他类似职位的意义可能不同。为了本文的目的,我们将数据验证定义为确保用于模型的训练数据与推断数据一致的过程。对于一些公司和一些用例,如果数据来自同一来源,则无需担心此问题。因此,这个过程必须发生,并且只有在数据来自不同来源时才有用。数据不会来自相同来源的一些原因包括,如果你的训练数据是历史数据和自定义数据(例如:从现有数据中提取的特征),和/或你的推断数据来自实时表格,而训练数据是快照数据。总之,这种不匹配的原因有很多,制定一个规模化的过程来确保你在推断时提供给模型的数据是你——即训练好的模型数据所期望的,将极有益处。
启用数据收集
照片由 Dennis Kummer 提供,来自 Unsplash [2]。
有很多方法可以启用数据收集。但再次强调,我们首先要定义收集的数据,即推理数据。我们预计我们的训练数据(包括训练集和测试集)已经存储在某处,也许是在 S3、文件存储工具、数据库中的临时表,甚至是 CSV 文件等。
然而,推理数据在预测时使用更为常见,但不一定会被存储。因此,你需要启用其收集。类似于训练数据的示例,你可以将推理数据存储在数据库表、数据湖、CSV 文件、JSON 文件、S3、已存储的 Kafka 流等。选择哪种收集方式将取决于你公司提供的选择。
还有一些工具、库和 Python 函数可以启用推理数据存储。
下面是一些可以用来收集推理数据的工具:
-
Kafka 流入数据库:调用模型的服务端点可以将数据加载到 Kafka 流中,然后从那里,你可以将数据加载到数据库表中
-
Amazon Web Services 捕获内容 [3]:这个工具非常用户友好,并允许你在模型代码中添加一个参数。你将需要使用 Amazon SageMaker 来使用这个工具。
代码示例如下,你可以看到如何简单地将参数添加到 AWS SageMaker 模型代码中,如果这是你正在使用的工具:
# Configuration object passed in when deploying Models to SM endpoints
data_capture_config = DataCaptureConfig(
enable_capture = enable_capture,
sampling_percentage = sampling_percentage, # Optional
destination_s3_uri = s3_capture_upload_path, # Optional
capture_options = ["REQUEST", "RESPONSE"],
)
# Example code from AWS [3]
对于具体工具的详细介绍,我将在下一篇文章中展开,并重点介绍代码。目前,在你的数据验证计划中,你需要确保有一种方法来捕获和存储推理数据,以及你的训练数据。
设定基准线
变量示例。截图由作者提供 [4]。
基准线有助于理解你的训练数据。理解你的数据应该如何在视觉上呈现并记住它可能会让人感到困惑,因此最好创建一个从数据中导出描述性统计的自动化过程。
基准数据示例
对于分类列(字符串、对象、类别等),你可以为已知数据和预期缺失的数据创建基准线:
- 唯一的 X 列值,例如:“八月、九月”
对于数值列(整数、浮点数):
-
列 X 平均值
-
列 Y 标准差
-
列 X、Y 最小值、最大值等,例如:20,000.60
尽管这看起来相当简单,但当你的模型和数据变得更大时,尽早跟踪数据将是有益的,这样你可以理解何时以及为何你的推断数据中会出现异常值。
上述是创建基线统计的一般示例,下面将介绍一个非常易于使用的具体工具
Pandas Profiling:
这个 Python 库 [5] 为你执行了大量的探索性工作,这将作为你的数据基线信息。总体而言,这个库会查看以下类型的基线统计。
分位数:
-
最小值
-
第 5 百分位数
-
Q1,中位数,Q3,第 95 百分位数
-
最大值
-
范围
-
四分位距(IQR)
描述性:
-
标准差
-
变异系数(CV)
-
峰度
-
平均值,中位数绝对偏差(MAD)
-
偏度
-
总和
-
方差
-
单调性
# install library
#!pip install pandas_profiling
import pandas_profiling
import pandas as pd
import numpy as np
# create data
df = pd.DataFrame(np.random.randint(0,200,size=(15, 6)), columns=list('ABCDEF'))
# run your report!
df.profile_report()
检测异常值
由 Will Myers 在 Unsplash 拍摄的照片 [6]。
现在你已经收集了训练数据和推断数据,并且有了预期的基线值,你可以知道当推断中的值变为异常值时。
需要记住的是,训练数据本身被假设为 100%准确,因为它被用作真相源。
有了收集的推断数据,你可以使用大量工具、库、模块等,将推断数据与训练数据进行比较。
为了进一步说明这一点,我会给出两个例子,一个没有问题,另一个是异常值。
这里是一个分类数据和数值数据的非异常值示例:
-
训练数据分类特征(months)→ “August”
-
推断数据分类特征(months)→ “September”
-
训练数据数值特征(temperature)→ 100
-
推断数据数值特征(temperature)→ 110
这里是一个分类数据和数值数据的异常值示例:
-
训练数据分类特征(months)→ “August”
-
推断数据分类特征(months)→ “AUgust”
-
训练数据数值特征(temperature)→ 100
-
推断数据数值特征(temperature)→ 1000000000
再次看似显而易见,但想象一下你有 50+个特征和 100,000 行数据。几乎不可能检查所有数据是否符合预期。
此外,为了更突出这个例子,虽然“August”与“AUgust”看起来相似,但模型当然不会将它们视为相同的值。对于数值数据,我们也不希望我们的温度读数如此之高。
虽然这些值在推理时显然不正确,但模型仍然可以对它们进行预测(取决于你的模型),这使得知道何时发生这种情况变得更加困难。
作为额外内容,你需要设置一个过程以确保你的预测与你的期望一致。
举个具体的例子,你可以使用Amazon SageMaker Model Monitoring工具,或者如果你在其他平台,可以使用像TensorFlow Data Validation [7]这样的开源库。
stats = tfdv.generate_statistics_from_tfrecord(data_location=path)
tfdv.visualize_statistics(stats)
other_stats = tfdv.generate_statistics_from_tfrecord(data_location=other_path)
anomalies = tfdv.validate_statistics(statistics=other_stats, schema=schema)
# You cannot run this code without importing the libraries, however,
# it is the outline for using the anomlay detection or outlier detection.
# Code from AWS [7]
以下是你为什么要使用这个特定库的主要用例[7],而且正如我之前所说,AWS 在这里很好地解释了原因:
- “验证新数据以确保我们没有突然开始接收到不良特征”
总结
了解何时发生离群值可能非常重要,因为你的模型可能会预测出由于数据不准确而可能造成伤害的结果。你不希望在涉及安全的欺诈检测中出现这种问题。
这篇文章作为一个快速大纲和提醒,以确保你的数据不仅对训练模型有用,也适用于预测时。
总结一下,我们在这篇文章中讨论了以下内容:
* Enabling Data Collection
* Setting a Baseline
* Detecting Outliers
我希望你觉得我的文章既有趣又有用。如果你的数据验证经验相同或不同,请随时在下方评论。为什么会这样?还有哪些其他话题你认为应该更多讨论?这些确实可以进一步澄清,但我希望我能对数据科学家数据验证的常见大纲提供一些见解。如果你希望看到关于这个过程中特定工具的文章,也可以随时评论。
我与这些公司没有任何关联。
请随时查看我的个人资料, Matt Przybyla,以及其他文章,并通过以下链接订阅以接收我的博客的电子邮件通知,或者通过点击屏幕顶部的订阅图标*,如有任何问题或评论,请在 LinkedIn 上联系我。
感谢阅读!
订阅链接: datascience2.medium.com/subscribe
推荐链接: datascience2.medium.com/membership
(如果你在 Medium 上注册会员,我会获得佣金)
参考文献
[1] 图片由 NordWood Themes 提供,在 Unsplash 上,(2018)
[2] Matt Przybyla,变量示例的截图,(2020)
[3] 2023, 亚马逊网络服务公司或其附属公司。版权所有,从实时端点捕获数据,(2023)
[4] 由 Stephen Dawson 拍摄于 Unsplash,(2018)
[5] pandas-profiling,GitHub 上的文档和所有贡献者,(2020)
[6] 由 Will Myers 拍摄于 Unsplash,(2018)
[7] TensorFlow,除非另有说明,本页面内容按 知识共享署名 4.0 国际许可证 许可,代码示例按 Apache 2.0 许可证 许可。有关详细信息,请参阅 Google 开发者网站政策。Java 是 Oracle 及/或其附属公司的注册商标,TensorFlow 数据验证,(2023)
预测多个视野:以天气数据为例
原文:
towardsdatascience.com/forecast-multiple-horizons-an-example-with-weather-data-8d5fa4321e07
使用预测视野作为特征预测瑞士的降水量。
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 8 月 6 日
--
天气预报,作者:Giulia Roggia。已获许可使用。
-
介绍
-
示例:瑞士的降水量
介绍
传统方法
当我们想要预测时间序列的未来值时,我们通常对多个未来视野感兴趣,例如 1、2 或 3 个月后的情况。预测这些不同视野的传统方法是为每个目标视野训练一个单独的模型。
常见替代方案
一种常见的替代方法是训练一个短期视野的单一模型,然后通过递归应用(即将之前的预测作为输入来产生后续预测)将其扩展到多视野。然而,这种方法可能在生产系统中实施复杂,并且可能导致误差传播:在近视野上的误差可能对后续的视野产生不利影响。
另一种替代方法是用多变量模型同时预测所有视野。然而,支持多变量输出的模型种类有限,并且在数据处理和模型维护上需要额外的努力。
视野作为特征
一种更简单的方法是将为每个视野准备的数据拼接在一起,并添加一个新的“视野”特征。这种方法具有几个优点:
-
它易于理解和实施,因为它只需要训练和维护一个模型。
-
它可能提高预测准确性,因为模型是在更大的数据集上训练的。它甚至可以作为一种“数据增强”技术使用:如果你只对几个视野感兴趣,你仍然可以在训练阶段添加额外的视野来改善模型估计。
-
该模型可以用于预测其未经过训练的范围,这在你有多个范围需要预测时可能很有帮助。
这种方法是 全球模型 的另一种表现形式,但在多个预测范围的背景下,而不是多个时间序列。因此,它有类似的 缺点:
-
如果特定预测范围的性能开始下降,更新模型很难而且可能会影响其他范围的预测。
-
当出现新的预测范围时,可能需要完全重新训练(尽管这不是强制性的)。
-
你不能使用传统的预测模型(如 ARIMA、指数平滑等)。
示例:瑞士的降水量
在本节中,我们展示了“范围作为特征”技术的具体 Python 实现,并将其与每个范围训练一个模型的传统方法进行比较。
我们的目标是预测卢加诺的降水量,这是一座位于瑞士的城市。此示例中使用的数据由 MeteoSwiss 提供,数据可以在 这里 获得(数据使用已获许可)。
数据加载
让我们首先导入必要的库,以便处理数据、可视化数据和训练 LightGBM 模型(如果你对模型的选择感兴趣,可以查看这篇文章)。
import pandas as pd
import plotly.graph_objects as go
from lightgbm import LGBMRegressor
让我们将数据加载到 Pandas 数据框中:
def load_climate_data(path):
# Load data in a dataframe.
with open(path,"r", encoding="ISO-8859-1") as f:
data = f.readlines()
columns = data[27].split()
data = [v.split() for v in data[28:]]
data = pd.DataFrame(data, columns = columns)
# Fix time
data["time"] = pd.to_datetime(data.Year + "-" + data.Month.astype(str))
data = data.drop(columns = ["Year","Month"])
data = data.set_index("time")
# Fix types
data = data.replace("NA",None)
return data.astype(float)
DATA_PATH = "./data/climate-reports-tables-homogenized_LUG.txt"
data = load_climate_data(DATA_PATH)
快速数据探索
现在我们可以查看数据:
def show_data(data,title=""):
trace = [go.Scatter(x=data.index,y=data[c],name=c) for c in data.columns]
go.Figure(trace,layout=dict(title=title)).show()
# Let's visualize the data.
show_data(data,"Weather Data in Lugano")
卢加诺的温度和降水量。图片来源:作者。
我们可以看到,从 1864 年开始,我们有每月的温度和降水量数据(这几乎是 160 年!)。让我们利用 Plotly 的交互功能更仔细地查看。
过去 10 年卢加诺的温度。图片来源:作者。
过去 10 年卢加诺的降水量。图片来源:作者。
我们可以看到数据具有季节性,温度比降水量更规律,这符合预期。
由于降水量更难预测,它们也更有趣!让我们尝试预测未来 1、2 和 3 个月的值。
数据工程
让我们准备数据以训练预测模型。我们将使用滞后的降水量和温度值来预测未来的降水量:
-
对于降水量,我们考虑最近 3 个滞后值,以及目标月份之前 1 年、2 年和 3 年的滞后值。
-
对于温度,我们考虑最近 2 个滞后值,以及目标月份之前 1 年和 2 年的滞后值。
我们还添加了一个“month_horizon”特征,指示我们正在预测哪个月份(例如 1 月或 4 月),以帮助模型学习季节性。注意这与“作为特征的时间跨度”不同,后者取值为 1、2 或 3。
def build_features(data, horizon):
"""Build lagged features.
We depend on horizon due to relative lags shift.
E.g, if the horizon is equal to 1, the target value
of 12 months before corresponds to a lag of 11.
"""
# Here we hardcode values for simplicity, but everything could
# (and should) be parametrized.
precipitation_lags = [0, 1, 2, 12 - horizon, 24 - horizon, 36 - horizon]
temperature_lags = [0, 1, 12 - horizon, 24 - horizon]
# Concatenate precipitation and temperature features.
features = pd.concat(
[
build_lagged_features(data.Precipitation, lags=precipitation_lags),
build_lagged_features(data.Temperature, lags=temperature_lags),
],
axis=1,
)
# Add horizon_month as a feature.
features["horizon_month"] = (features.index.month + horizon - 1) % 12 + 1
# Trick to later allow concatenation of features for different
# target horizons.
features = features.rename(
columns={
f"Precipitation_lag_{12-horizon}": "Precipitation_lag_12_before_target",
f"Precipitation_lag_{24-horizon}": "Precipitation_lag_24_before_target",
f"Precipitation_lag_{36-horizon}": "Precipitation_lag_36_before_target",
f"Temperature_lag_{12-horizon}": "Temperature_lag_12_before_target",
f"Temperature_lag_{24-horizon}": "Temperature_lag_24_before_target",
}
)
return features
def build_lagged_features(series, lags):
return pd.concat(
[series.shift(lag).rename(f"{series.name}_lag_{lag}") for lag in lags],
axis=1,
)
我们现在可以为每个时间跨度构建目标和特征。
def build_target_features(data, horizon):
targ = build_target(data.Precipitation, horizon)
feat = build_features(data, horizon)
# Drop missing values generated by lags/horizon.
idx = ~(feat.isnull().any(axis=1) | targ.isnull())
feat = feat.loc[idx]
targ = targ.loc[idx]
return targ, feat
def build_target(series, horizon):
return series.shift(-horizon)
# Let's build the targets and features for each horizon.
HORIZONS = [1,2,3]
target_features = {h: build_target_features(data, h) for h in HORIZONS}
拆分训练集和测试集
我们将数据中最近 10 年的部分作为测试集:
def split_train_test(target_features, test_size):
targ_feat_split = {}
for horizon, (targ,feat) in target_features.items():
targ_train = targ.iloc[:-test_size]
feat_train = feat.iloc[:-test_size]
targ_test = targ.iloc[-test_size:]
feat_test = feat.iloc[-test_size:]
targ_feat_split[horizon] = targ_train, feat_train, targ_test, feat_test
return targ_feat_split
TEST_SIZE = 10 * 12
targ_feat_split = split_train_test(target_features, test_size=TEST_SIZE)
模型训练
由于我们只关心比较这两种不同的方法,因此两种情况下我们都保持默认超参数。
让我们开始传统方式训练模型,为每个时间跨度训练一个:
def train_models_by_horizon(targ_feat_split, model_params=None):
if model_params is None:
model_params = {}
# Train one model for each horizon
models_by_horizon = {}
for horizon, (targ_train,feat_train,_,_) in targ_feat_split.items():
model = LGBMRegressor(**model_params)
model.fit(feat_train, targ_train)
models_by_horizon[horizon] = model
return models_by_horizon
models_by_horizon = train_models_by_horizon(targ_feat_split)
现在让我们运行另一种方法:使用时间跨度作为特征的单一模型:
def train_model_across_horizons(targ_feat_split, model_params=None):
if model_params is None:
model_params = {}
# Concatenate data across horizons.
targ_train_all = []
feat_train_all = []
for horizon, (targ_train,feat_train,_,_) in targ_feat_split.items():
# Add horizon as a feature.
feat_train = feat_train.copy()
feat_train["target_horizon"] = horizon
targ_train_all.append(targ_train)
feat_train_all.append(feat_train)
targ_train_all = pd.concat(targ_train_all)
feat_train_all = pd.concat(feat_train_all)
# Train a single model.
model = LGBMRegressor(**model_params)
model.fit(feat_train_all, targ_train_all)
return model
model_shared = train_model_across_horizons(targ_feat_split)
在测试集上进行预测
让我们用这两种方法对测试集进行预测:
def predict_models_by_horizon(targ_feat_split, models_by_horizon):
preds = {}
for horizon, (_,_,_,feat_test) in targ_feat_split.items():
preds[horizon] = models_by_horizon[horizon].predict(feat_test)
return preds
def predict_model_across_horizons(targ_feat_split, model):
preds = {}
for horizon, (_,_,_,feat_test) in targ_feat_split.items():
# Add horizon as a feature.
feat_test = feat_test.copy()
feat_test["target_horizon"] = horizon
preds[horizon] = model.predict(feat_test)
return preds
preds_by_horizon = predict_models_by_horizon(targ_feat_split, models_by_horizon)
preds_model_shared = predict_model_across_horizons(targ_feat_split, model_shared)
错误分析
现在让我们评估预测性能。首先,我们将输出合并成便于比较的格式。
# Let's combine the output in a convenient format.
output = {}
for horizon in HORIZONS:
df = targ_feat_split[horizon][2].rename("target").to_frame()
df["pred_model_by_horizon"] = preds_by_horizon[horizon]
df["pred_model_shared"] = preds_model_shared[horizon]
output[horizon] = df
下面您可以看到时间跨度为 1 个月的输出数据框的开头部分。
时间跨度为 1 个月的输出数据框。图片由作者提供。
我们现在计算并打印整体和每个时间跨度的平均绝对误差(MAE)。
def print_stats(output):
output_all = pd.concat(output.values())
mae_by_horizon = (output_all.target - output_all.pred_model_by_horizon).abs().mean()
mae_shared = (output_all.target - output_all.pred_model_shared).abs().mean()
print(" BY HORIZON SHARED")
print(f"MAE overall : {mae_by_horizon:.1f} {mae_shared:.1f}\n")
for h,df in output.items():
mae_by_horizon = (df.target - df.pred_model_by_horizon).abs().mean()
mae_shared = (df.target - df.pred_model_shared).abs().mean()
print(f"MAE - horizon {h}: {mae_by_horizon:.1f} {mae_shared:.1f}")
# Let's show some statistics.
print_stats(output)
这将产生:
我们看到跨时间跨度共享的模型总是导致较低的误差。这在某种程度上是预期的,因为它是在更大的数据集上训练的。
让我们看看一些预测结果:
# Let's have a look at the predictions.
for horizon, df in output.items():
show_data(df,f"Predictions at Horizon {horizon}")
这里为了简洁,只显示时间跨度为 3 个月的预测:
时间跨度为 3 个月的预测。图片由作者提供。
我们看到模型仍然无法捕捉到极端事件(如 2014 年 8 月)。这并不令人惊讶,因为:
-
我们对系统的实际状态了解非常有限。
-
极端事件很难通过机器学习进行预测,因为定义上在训练集中观察到的实例有限。
下一步
我们可以尝试几个方法来提高预测性能。以下是一些非详尽的列表:
-
通过使用 L2 损失来惩罚大误差。
-
目标转换,例如取平方根。
-
包括月内数据。包括额外的数据来源。
-
使用在更多目标上训练的全局模型。
-
更频繁地进行再训练,例如每年。
-
超参数调整。
要评估这些变化是否提高了预测准确性,您应该依赖于一个可靠的回测策略。
本示例中使用的完整代码可以在 这里 找到。
喜欢这篇文章吗? 查看我的其他文章 并关注我以获取更多内容! 点击这里 阅读无限制的文章,并在不增加您额外成本的情况下支持我 ❤️
像大师一样预测多个时间序列
原文:
towardsdatascience.com/forecast-multiple-time-series-like-a-master-1579a2b6f18d
从局部到全局算法
·发表于Towards Data Science ·阅读时间 30 分钟·2023 年 4 月 26 日
--
图片来源:Jesús Rocha 于Unsplash
我在商业中处理多个时间序列的预测(准确来说,是需求预测)。在我之前的文章中Sell Out Sell In Forecasting,我介绍了我在雀巢实施的需求预测方法。在这篇文章中,我想向你介绍目前用于预测多个时间序列的通用(这并不意味着理想)算法——例如最先进的时间序列算法。对于零售商或制造商来说,预测需求对业务至关重要。它允许他们制定更准确的生产计划并优化库存。不幸的是,许多公司(并非雀巢 😃 )并未意识到这个问题,他们仍然使用简单统计的电子表格。如果他们能改变这种情况,他们可以显著降低成本。毕竟,仓储和过时产品——这也是额外的成本。
如何预测多个时间序列,作者提供的图片
很难找到一个数据科学领域的人不熟悉Scikit-learn。对于数据框架,你可以使用Scikit-learn来完成机器学习中涉及的大部分元素——从预处理到超参数选择、评估和模型预测。我们可以将线性回归、决策树或支持向量机(SVM)分配给变量model,并每次使用相同的方法,如fit和predict。我们有很大的灵活性,但也有一种简单的方式来实现解决方案。
对于时间序列来说,情况是不同的。如果你在实验并想比较不同的算法,算法本身不仅仅是一个问题。
如果你开始处理时间序列,你需要对它们进行处理,例如重新采样或填补缺失值——Pandas对此非常有用。
如果你想进行分解、可视化 ACF/PACF,或检查平稳性测试,那么Statsmodels库将非常有用。
对于可视化,你可能会使用Matplotlib,即使不是这个库,也有许多建立在它之上的库。
当你想使用不同的算法时,乐趣才开始。当你想使用ARIMA时,你可能会使用pmdarima,Prophet是另一个库。典型的Machine Learning 算法可以在之前提到的Scikit-learn中找到,但你也可能想使用像LightGBM或CatBoost这样的提升模型。对于Deep Neural Networks 和最新论文中的架构,PyTorch Forecasting值得使用。
WOW🤯 你可能需要的库非常多。如果你想能够使用上述提到的库,这将是一个大量的工作,因为大多数库使用不同的 API、数据类型,并且对于每个库中的模型,你都必须准备自己的回测和超参数选择函数。
Library Darts,由作者提供的图片,灵感来自于图书馆文档
这里我们得到帮助的是Darts,它试图成为时间序列的Scikit-learn,其目的是简化时间序列的工作。它的功能通常基于其他库,例如,它使用Statsmodels进行分解。如果某些功能在其他库中没有实现,Darts也能很好地与其他库协作,你可以将其与Matplotlib和Seaborn互相配合使用进行比较。
在必要的地方,它们有自己的实现,但它们不想重新发明轮子,而是使用其他流行时间序列库中已经存在的东西。
这一概念在时间序列领域并不新鲜,还有其他很好的库,例如sktime、GluonTS或nixtla,但在我看来,Darts的入门门槛最低,功能也更为完善。这不是对这个库的广告,归根结底,你的预测应该为你工作的企业带来价值。你也可以从头开始用代码编写这些模型。我将使用 Darts 进行以下示例,但你也可以在上述提到的库中找到这些模型(全部或部分)。如果我们想训练多个本地模型,我认为 Darts 库在优化计算方面还有改进的空间——可以尝试nixtla库,它提供与 Spark、Dask 和 Ray 的兼容性。
从我的角度来看,Darts 已经是一个成熟的库,并且仍在不断开发中,只需查看变更日志即可。现在你可以按照标准方式进行安装:
pip install darts
一旦我们在环境中安装了库,就可以导入并在实践中使用它。
import darts
单变量与多变量时间序列
单变量与多变量时间序列,作者提供的图像
如上图所示,基于Walmart 数据集,你可以看到单变量和多变量时间序列。现在,许多问题涉及同时处理多个点。这些数据可以来自各种过程,可以是这个例子和我日常工作的需求预测,也可以是能源消耗预测、公司股市收盘价、从车站租用的自行车数量等等许多其他问题。
除了时间序列本身,我们还可能有其他变量,其中一些可能已知未来值,而其他仅在过去可用——稍后会详细讲解。
在这篇文章中,我想向你展示预测多个时间序列的不同方法,但我希望它是实用的——以便你不仅仅停留在理论层面。所以让我们导入所有后续使用的库——包括 Darts 和其他数据科学家熟知的库。
# multiprocessing
from joblib import Parallel, delayed
# data manipulation
import numpy as np
import pandas as pd
from darts import TimeSeries
from darts.utils.timeseries_generation import datetime_attribute_timeseries
# data visualization
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm
# transformers and preprocessing
from darts.dataprocessing.transformers import Scaler
# models
from darts.models import NaiveSeasonal, StatsForecastAutoARIMA, ExponentialSmoothing, Prophet #local
from darts.models import LightGBMModel, RNNModel, NBEATSModel, TFTModel #global
# likelihood
from darts.utils.likelihood_models import GaussianLikelihood
# evaluation
from darts.metrics import mape
# settings
import warnings
warnings.filterwarnings("ignore")
import logging
logging.disable(logging.CRITICAL)
现在让我们加载数据集,这个数据集涉及需求预测,并来自Kaggle。如果你同意 Kaggle 上的比赛条款,那么你也可以下载这个数据集。
dataset = pd.read_csv('store_item_demand.csv')
# set the column type for column with date
dataset['date'] = pd.to_datetime(dataset['date'], format='%Y-%m-%d')
# sort values and reset index
dataset.sort_values(by=['date', 'store', 'item'], inplace=True)
dataset.reset_index(drop=True, inplace=True)
# creation of an auxiliary table with hierarchy and aggregated sales totals
hierarchy_df = dataset.groupby(['store', 'item'])[['sales']].sum()
hierarchy_df = hierarchy_df.reset_index(drop=False).sort_values(by=['sales'],
ascending=False).reset_index(drop=True)
共有 10 家商店,每家商店有 50 种商品,总计 500 个时间序列。
让我们来看一下在所有商店商品组合中销售最少、中等和最多的 10 种商品。要找出时间序列中的关系,通常只需观察它,因为这已经能告诉我们很多信息,比如趋势或季节性,但往往还有更多。
fig, ax = plt.subplots(figsize=(30, 10))
for single_ts in list(np.arange(0, 10)) + list(np.arange(245, 255)) + list(np.arange(490, 500)):
single_ts_df = pd.merge(dataset, hierarchy_df.loc[[single_ts], ['store', 'item']], how='inner', on=['store', 'item'])
ax.plot(single_ts_df['date'], single_ts_df['sales'], color='black', alpha=0.25)
ax.set_xlim([dataset['date'].min(), dataset['date'].max()])
ax.set_ylim([0, dataset['sales'].max()])
plt.show()
这些时间序列之间有很多相似性。我们将稍后检查是否存在季节性(周几或一年中的周/月)或趋势,但你可以直观地想象,从所有时间序列中学习的模型将比仅从其历史中学习的模型更好地预测单个时间序列。
我为EDA(探索性数据分析)创建了数据集的副本。然后,我使用MinMAXScaler对时间序列进行缩放,使所有时间序列可以互相比较。最后,我创建箱型图,以检查是否存在趋势和季节性。
# make copy of df
dataset_scaled_EDA = dataset.copy()
# min max value calculation
dataset_scaled_EDA['min_sales'] = dataset_scaled_EDA.groupby(['store', 'item'])['sales'].transform(lambda x: x.min())
dataset_scaled_EDA['max_sales'] = dataset_scaled_EDA.groupby(['store', 'item'])['sales'].transform(lambda x: x.max())
# scale
dataset_scaled_EDA['sales_scaled'] = (dataset_scaled_EDA['sales'] - dataset_scaled_EDA['min_sales'])/(dataset_scaled_EDA['max_sales'] - dataset_scaled_EDA['min_sales'])
# add info about year, week of year and day of week
dataset_scaled_EDA['year'] = dataset_scaled_EDA['date'].dt.year
dataset_scaled_EDA['month'] = dataset_scaled_EDA['date'].dt.month
dataset_scaled_EDA['day_of_week'] = [d.strftime('%A') for d in dataset_scaled_EDA['date']]
dataset_scaled_EDA['day_of_week'] = pd.Categorical(dataset_scaled_EDA['day_of_week'],
categories=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
ordered=True)
# visualize
fig, ax = plt.subplots(1, 3, figsize=(30, 10))
sns.boxplot(x='year', y='sales_scaled', data=dataset_scaled_EDA, ax=ax[0]).set(
xlabel='Year',
ylabel='Scaled Sales'
)
ax[0].set_title('Box plot for years (trend)')
sns.boxplot(x='month', y='sales_scaled', data=dataset_scaled_EDA, ax=ax[1]).set(
xlabel='Month',
ylabel='Scaled Sales'
)
ax[1].set_title('Box plot for months (seasonality)')
sns.boxplot(x='day_of_week', y='sales_scaled', data=dataset_scaled_EDA, ax=ax[2]).set(
xlabel='Day of week',
ylabel='Scaled Sales'
)
ax[2].set_title('Box plot for day of week (seasonality)')
ax[2].set_xticklabels(ax[2].get_xticklabels(), rotation=30)
plt.show()
是的,有趋势和两种季节性。如果你认为这些时间序列很容易预测——你是对的。本文的目的是向你展示预测多个时间序列的最流行的方法。数据并不总是像股票市场指数那样简单,但那是另一个话题。
我认为这种探索足以理解模型应该学习哪些关系。
在这里,我们有多个时间序列(多个商店中的多个商品销售)。对于每一个时间序列,我们从 Pandas DataFrame 中创建一个TimeSeries对象。这种类型是 Darts 库中的模型所需的。然后将这些时间序列保存到列表中。
dataset_ts = dataset.copy()
dataset_ts = TimeSeries.from_group_dataframe(df=dataset_ts,
group_cols=['store', 'item'],
time_col='date',
value_cols='sales')
dataset_ts
处理多个时间序列可能很有帮助,但通常也会带来问题。当我们只有一个时间序列时,我们有很多时间来处理它。查看它,验证趋势和季节性,并处理异常。我们可以优化我们的预测。对于多个序列,这种方法变得不可行。我们希望方法尽可能自动化,但这样可能会错过细节,比如异常,或者我们不应该以相同的方式处理每个时间序列。可能还有更多典型的问题:缺失数据、数据漂移和稀有事件(黑天鹅事件)。更多的序列可能对我们有帮助,因为我们的模型可以使用更多的数据,因此会有更多的代表性观察数据来识别特定模式。以我的工作为例——为了预测产品 X 在下周因促销造成的需求,我们的模型还可以利用其他促销的历史效果。
所以问题来了——如何预测多个时间序列?
你可能经常会问自己我是否有一个 多变量 或 多元 时间序列? 这些问题是合理的,但答案并不总是明确的。当你的时间序列来自单一过程,相互关联、相关,并且相互作用时,答案将是我的时间序列是 多元。
当你在多个商店预测产品 X 的销售时,你有一个多时间序列,但当你有一个额外的产品 Y 时,对于单个商店来说,你有一个多变量时间序列,因为一个产品在商店中的销售可能影响另一个产品的销售,或者这只是一个假设。
如何评估预测结果?
在我们深入讨论不同的方法和模型之前,让我们先讨论如何衡量预测的质量。这是一个回归问题,因此我们仍然会将预测结果与真实值进行比较,这一点毫无疑问。你可以使用回归问题中熟悉的指标,如RMSE(均方根误差), MSE(均方误差), MAE(平均绝对误差),或者更典型的时间序列指标,如MAPE(平均绝对百分比误差), MARRE(平均绝对范围相对误差), 或MASE(平均绝对缩放误差)。进一步的讨论将使用MAPE作为评估指标。本文不是关于需求预测的,但由于数据和我的经验,有很多相关参考。因此,你可以考虑为该问题选择什么指标。始终选择能够反映业务目标的指标。在这篇文章中,Nicolas Vandeput 描述了需求预测中使用的 KPI 指标。
我们可以将这种方法扩展到多个序列,然后一次性计算所有序列的指标,或者分别对每个序列计算指标后再进行汇总。所以让我们继续探讨如何评估单个序列,然后再将其扩展到多个序列。
是的,这是一个回归问题,你可能会想为什么我解释这一点。在时间序列中,时间扮演了关键角色。数据是相对于时间排序的,观测值是相互关联的。因此,分割训练/测试集并使用交叉验证时无法使用随机化,因为这样会导致数据泄漏。
预测模型的评估,图像由作者提供
首先,将数据分为训练集和测试集,很简单,对吧?模型在训练集上进行拟合,在测试集上进行测试。我们可以按比例划分,例如,测试集包括最后 20% 的数据,或者指定测试集的起始日期——可能由于业务考虑,测试集为过去一年数据会更重要。
在我们的例子中,测试集将是最后一年。
first_test_date = pd.Timestamp('2017-01-01')
train_dataset_ts, test_dataset_ts = [], []
for single_ts in tqdm(dataset_ts):
# split into train and test tests
single_train_ts, single_test_ts = single_ts.split_before(first_test_date)
train_dataset_ts.append(single_train_ts)
test_dataset_ts.append(single_test_ts)
从训练集分离验证子集,用于选择超参数。对于在 Darts 中实现的模型,我们可以使用gridsearch方法,但对于基于神经网络的模型,推荐使用Optuna。gridsearch 方法实际上具备了我们所需的一切,无论选择哪个模型都能正常工作。
重要的是:
-
parameters ➜ 一个包含待检查超参数的字典
-
series ➜ TimeSeries 对象或包含 TimeSeries 对象的列表(如果算法是全局的)
-
start ➜ Pandas Timestamp 定义第一次预测发生的时间,或者 float 作为第一次预测之前观察值的比例。
-
forecast_horizon ➜ 模型预测的时间范围数量(int)
-
stride ➜ 下一次预测之间的偏移量。为了使一切符合数据科学的艺术,并最能反映算法的实际操作,stride 应该等于 1。但请记住,在每一步之后你的算法都会重新训练,而且你的超参数网格可能有很多组合,这通常需要很长时间。因此,出于常识考虑,stride 可以大于 1,尤其是因为此目的在于选择最佳超参数,结果可能(但不一定)在 stride 等于 1 时与 stride 等于 5 时相同。
-
metric ➜ 函数,用于比较真实值和预测值。然后根据这个指标选择最佳超参数。
model.gridsearch(
parameters={},
series= ,
start= ,
forecast_horizon= ,
stride= ,
metric=
)
那么如何在测试集上评估模型呢?我们有两种方法。
第一种方法是我们在训练集上拟合模型,并做一个覆盖测试集的预测。在以下示例中,我们将使用这个选项——计算速度更快,因为我们为每个时间序列生成一个预测。
第二种方法是我们测试不同的预测时间范围。我们拟合模型,做一个预测,重新训练,再做一个预测,以此类推,但我们必须在训练集结束之前开始训练算法,这样我们才能在相同的数据上比较时间范围——我们在比较相同的苹果。如果你从整个训练集开始训练,那么较远的时间范围有较少的观察值。在这种方法中,我们应该重新训练(特别是在局部模型中),因此这种方法中我们肯定会有更多的计算。
这也很简单,因为 darts 中的模型具有根据上述可视化返回历史预测的能力。你已经学习了一些在 gridsearch 方法中使用的变量,这些变量在这里也适用。然而,这里会有新的变量:
-
retrain ➜ 如果等于 True,则在每一步之后模型会重新训练,这最能反映现实情况。
-
overlap_end ➜ 如果等于 True,则预测可能超出测试集中的日期。如果我们对多个时间范围进行预测,而较远的时间范围超出测试集,而较近的时间范围不超出,这样的设置很有用。
-
last_points_only ➜ 如果等于 False,则返回所有时间范围的预测。
backtests_results = model.historical_forecasts(
series= ,
start= ,
forecast_horizon= ,
retrain=True,
overlap_end=True,
last_points_only=False,
)
然而,我们希望从感兴趣的时间范围中获取预测,并且在变量 backtests_results 中,有来自不同时间点的预测。要从特定的时间范围中获取预测,你可以使用我的函数 take_backtest_horizon。
backtests_5W ➜ 有针对测试集每个点的预测,这些预测是在 5 周前做出的。
def take_backtest_horizon(backtests, horizon, last_horizon, observations):
backtests_horizon_dates = [i.time_index[-1+horizon] for i in backtests[last_horizon-horizon:][:observations]]
backtests_horizon_values = [i.values()[-1+horizon] for i in backtests[last_horizon-horizon:][:observations]]
backtests_horizon = TimeSeries.from_times_and_values(times=pd.DatetimeIndex(backtests_horizon_dates), values=np.array(backtests_horizon_values))
return backtests_horizon
backtests_5W = take_backtest_horizon(
backtests=backtests_results,
horizon=5, # for which horizon we want a forecast, the number must be less than forecast_horizon
last_horizon=forecast_horizon, # forecast_horizon from historical_forecasts method
observations=len(test_series))
使用的变量
实际上,时间序列本身并不能完全自我解释。时间序列通常依赖于其他变量。这里我们没有这些变量,但了解如何在项目中遇到这些变量时进行区分是很有帮助的。如果我们没有通知模型即将到来的促销/降价,那么它将无法预测销售额的增加。如果你想预测股票公司的价格变化,添加来自技术分析或基本面分析的变量可能会有所帮助。这些变量依赖于价格,即你只能知道这些指标的过去值,毕竟,我们无法知道未来的公司财务报告或价格——我们希望进行预测 😃
时间序列算法使用的变量,图源作者
我们可以有也是时间序列的变量,即在不同时间点有不同的值,同时我们也可以有静态协变量(随时间保持不变),通常是分类变量。在我们的例子中,这将是商店 ID 和商品 ID。它们对全局模型非常重要。因为在每 100 个时间序列中,可能会有不同的关系,借助这些变量,你的模型可以区分不同的时间序列。
至于时间序列变量,我们可以区分协变量,其中有些已知未来(例如,我们可以知道未来的促销机制,也知道过去的促销机制)和仅已知过去的协变量(我们可以知道竞争产品的价格,但不知道它们未来的价格)。
使用的模型
局部算法与全局算法对比,图源作者
我们可以将机器学习分为监督学习、无监督学习和强化学习。进入详细内容后,我们可以将监督学习分为回归和分类。我们可以对时间序列预测进行类似的划分,即使用局部或全局算法来预测这些时间序列。
局部算法是针对单一时间序列进行拟合的,模型仅能预测这一时间序列。更多的时间序列意味着更多的模型。在这里我们看到优缺点,模型简单,但对于许多时间序列来说,这种方法变得难以维护。
全局算法则是一个模型可以拟合多个时间序列。所以如果我们有多个时间序列,我们可以有一个模型来预测所有这些序列。这种方法显然更灵活,例如,你可以使用迁移学习。对于时间序列,这意味着你在一个不同的时间序列上拟合模型,而不是进行预测。这里有一个使用示例。另一个与全局模型相关的重要点,因为我可能会忘记,并且这非常重要——那就是时间序列缩放。最常见的方法是MinMaxScaler,但你可以使用更适合你数据的东西。然而,我不会在这里详细说明如何缩放时间序列,这肯定是另一个文章的话题。我们来考虑一下为什么我们应该缩放时间序列。答案可能很简单,许多全局算法是神经网络,这就是我们缩放数据的原因,就像我们对卷积神经网络的像素进行处理一样。然而,这还不是全部。然而,我们可以使用像随机森林这样的模型(非参数模型),我们仍然应该缩放它们。但停下来,为什么?毕竟,对于这些类型的模型,你不需要缩放变量。我们应该缩放时间序列的原因是为了让模型学习关系,而不是尺度。例如,对于季节性关系,可能是在夏季月份,值比冬季月份平均高 150%。另一个例子是,3 次显著增加后跟随一次下降。如果我们不缩放时间序列,模型很难学习这些关系。这是一种与表格数据中变量缩放略有不同的方法,因为在这里我们单独缩放每个时间序列。如果我们使用前面提到的MinMaxScaler,那么对于训练集中的每个时间序列,最大值是 1。所以让我们缩放我们的数据,这将被全局模型使用。
scaler = Scaler() # MinMaxScaler
train_dataset_ts_prepared = scaler.fit_transform(train_dataset_ts)
test_dataset_ts_prepared = scaler.transform(test_dataset_ts)
dataset_ts_prepared = scaler.transform(dataset_ts)
一会儿你将阅读到最受欢迎的局部和全局算法。虽然不可能描述所有可能的算法,但有一些算法常被专家使用,并且通常能满足预期。
没有免费的午餐定理
没有一个答案——这个模型是最好的,不要使用其他模型。然而,如果你正在创建一个 MVP——最好从简单的东西开始。
在以下示例中,我没有在验证集上选择最佳超参数,而是使用了默认模型。所以如果你告诉我模型可能有更好的结果——我已经同意你的观点。
局部模型
在我们深入具体的局部模型之前,我为你准备了函数,以便使用多处理来加快所有核心的计算速度。
forecast_horizons = len(test_dataset_ts[0])
def _backtests_local_estimator(_estimator, _ts_set, _split_date, _horizons, _single_forecast):
model = _estimator
if _single_forecast:
model.fit(_ts_set.split_before(_split_date)[0])
backtests_single_ts = model.predict(_horizons)
else:
backtests_single_ts = model.historical_forecasts(series=_ts_set,
start=_split_date - np.timedelta64(_horizons-1, 'D'),
verbose=False,
overlap_end=False,
last_points_only=True,
forecast_horizon=_horizons,
retrain=True)
return backtests_single_ts
def backtests_multiple_local_estimators(estimator, multiple_ts_sets=dataset_ts, split_date=first_test_date, horizons=forecast_horizons, single_forecast=True):
backtests_multiple_ts = Parallel(n_jobs=-1,
verbose=5,
backend = 'multiprocessing',
pre_dispatch='1.5*n_jobs')(
delayed(_backtests_local_estimator)(
_estimator=estimator,
_ts_set=single_ts_set,
_split_date=split_date,
_horizons=horizons,
_single_forecast=single_forecast
)
for single_ts_set in multiple_ts_sets
)
return backtests_multiple_ts
我将使用这个函数为本地模型生成测试集的单个预测。此外,你可以用它生成多个历史预测(第二种方法,变量single_forecast应设置为False)。不过,我在这里不这样做,因为这会花费很多时间。
如果你使用 Cluster 和 Spark,那么你可以使用 Spark UDF 来显著加快计算速度。
我知道,你可能想跳到模型部分。最后但同样重要的是——一个评估我们预测结果的函数。我将使用MAPE作为评估指标,然而,如果你在做需求预测项目,WMAPE或MAE无疑更接近业务预期。
def get_overall_MAPE(prediction_series, test_series=test_dataset_ts):
return np.round(np.mean(mape(actual_series=test_series,
pred_series=prediction_series, n_jobs=-1)),
2)
基准线
好吧,但为什么要做神经网络,如果一个更好的主意是从一年前预测值呢?这正是我们首先创建这样一个模型的原因。当你处理实际数据时,你最好也从这样的方式开始(它也可以是训练集中的最后一个值,NaiveDrift 如果有趋势,或几种简单方法的组合)。然后如果你转向更高级的方法,你可以评估它比简单方法好多少,因为例如,如果你从神经网络开始,其 MAPE 为 10%,那么我的问题(可能也是利益相关者的问题)是——这好吗?
我们的模型(一个时间序列=一个模型)将重复一年前的值。
backtests_baseline_model = backtests_multiple_local_estimators(estimator=NaiveSeasonal(K=365))
print(f'overall MAPE: {get_overall_MAPE(backtests_baseline_model)}%')
整体 MAPE: 22.42%
现在让我们在测试集中可视化时间序列的预测值和实际值,这些时间序列具有最高的总销售额。
这看起来还不错。
ARIMA
视觉化 ARIMA 算法的工作原理,图像来源于作者
ARIMA 是一个统计模型,以其简单性而广受欢迎且强大。当你听到ARIMA时,它可能指的是这个模型,但也可能是ARIMA的扩展集合。这个集合包括ARIMAX(考虑额外变量)、SARIMA(考虑季节性)或VARIMA(用于多变量时间序列)。但让我们回到ARIMA(AutoRegressive Integrated Moving Average),这就是一切的起点。如果你理解得很好,那么使用之前提到的模型应该不会有问题。
许多文章已经对此算法进行了阐述。我想给你这个模型背后的直观理解。我希望最终你能轻松地在代码中实现它,并理解它是如何工作的。我打算从最后开始。我自己记得在招聘过程中有过一个关于这个问题的疑问,当时我还没有理解它。ARMA 模型只能与平稳时间序列一起使用,因此我们有一个组件——积分(I),它通常(但不总是)将非平稳时间序列转换为平稳时间序列。ARMA是模型,而I部分负责为建模准备数据(当然,如果需要的话)。你应该知道几个问题的答案,那就是时间序列平稳或非平稳的含义,以及积分(I)组件进行的转换类型。所以让我们从平稳性开始。
如果值的分布(均值和方差)在时间上是不变的,那么时间序列就是平稳的。
因此,如果存在趋势和/或季节性,那么时间序列就是非平稳的。要检查时间序列是否平稳,最简单的方法是将其可视化,并在图上添加移动平均和移动标准差。如果它们随时间保持不变(或接近不变),那么你可以得出你的时间序列是平稳的结论。这种方法可能显得天真,并且并不总是有效,因为如果对滚动统计数据使用过大的窗口,你可能会认为时间序列是平稳的,而实际上并非如此。另一种方法是将时间序列拆分成随机分区,对每个分区计算上述统计数据。最后一种方法是计算扩展的迪基-福勒(ADF)检验。如果我们的时间序列仍然不是平稳的,我们需要使用积分(I)组件怎么办?它通过差分来使时间序列平稳,即计算观察值之间的差异。如果我们的时间序列仍然不是平稳的呢?我们可以选择d的阶数,这表示我们对时间序列进行差分的次数。
这个长段落是关于积分(I),它准备数据以供自回归(AR)和移动平均(MA)组件使用。AR是对最后p个值的线性回归,这些值被称为滞后。当前值与最后的值相关联,并且依赖于这些值。MA是补充性的,考虑了预测中q个最后的误差(假设为白噪声),以更好地预测当前的时间点。
选择AR的阶数p时,我们使用PACF(Partial AutoCorrelation Function),而选择MA的阶数q时,我们使用ACF(AutoCorrelation Function)。在大学课程之外,我们在实际操作中不太可能这样做,因为我们有AutoARIMA可以为我们选择p、d和q。
让我们回到实践中,按照与基线模型相似的方式来实现。正如你可能已经了解到的,由于 Darts 库,这个过程非常简单。
backtests_arima = backtests_multiple_local_estimators(estimator=StatsForecastAutoARIMA())
print(f'overall MAPE: {get_overall_MAPE(backtests_arima)}%')
总体 MAPE: 28.18%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_arima[0].plot(label='Forecast', color='green')
plt.show()
如果我选择参数m(季节性差分的周期),结果可能会更好。你可以尝试一下,并告诉我结果的变化情况。
指数平滑
可视化指数平滑(ETS)算法的工作原理,图片由作者提供
指数平滑是另一种用于单变量时间序列的模型家族。你可以在术语ETS(E-误差,T-趋势,S-季节性)下找到“这个家族”。在这种方法中,观察值被赋予权重,较旧的观察值权重较低,因为它们按指数衰减。我们可以区分三种类型:一种简单的类型假设未来将类似于近期值,一种扩展类型处理趋势,最后一种还处理季节性。我将会在稍后描述这三种类型,不过现在先插一个小插曲。在M-4 比赛(Makridakis 比赛,这是最著名的时间序列预测比赛)中,Slawek Smyl 获胜,他提出了ES-RNN,这是指数平滑与递归神经网络的混合体。
现在我们回到话题的第一个类型,即简单指数平滑。作为基线模型,我们可以选择一个总是预测训练集中的最后一个值的模型,这是一种稍显天真的方法,但可以给我们带来不错的结果。另一种方法是计算整个训练集的平均值,但这样的话,最近的观察值和最古老的观察值会被赋予同等的重要性。指数平滑结合了这两种方法,赋予最近的观察值更大的权重,权重会随着观察值的古老程度呈指数下降,这意味着最古老的观察值将拥有最小的权重。它使用α参数,其范围在 0 到 1 之间。值越高,最新值对预测的影响越大。请查看上面的图形,那里也有公式。这些公式非常容易理解,通常这些模型能给出不错的结果。在我们进入更高级的模型之前,我们应该在这里稍作停留,因为如果一个简单的模型给出的结果与非常先进的模型(例如深度神经网络)相同,那么我们应该保持使用更简单的模型,因为它们的操作对我们来说更可预测,并且更多的人能够理解它们的运作。
SES 本质上无法处理数据中的趋势。如果存在上升趋势,则预测会低估,因为它没有包含这种增加。因此,我们有另一种模型,即双重指数平滑。它有一个额外的因素,用于考虑趋势的影响。我们使用β参数,它控制趋势变化的影响。因此我们有两个公式,一个用于水平(水平方程),另一个用于趋势(趋势方程)。
三重指数平滑也考虑了季节性。你可以将它称为 Holt-Winters 的季节性方法。这里另一个参数γ进入了公式。这种方法允许水平、趋势和季节性的模式随时间变化。像趋势一样,季节性可以是加法的或乘法的,但在这里我不会描述详细信息,假设你知道区别或可以轻松找到它们。我只是不想把这篇文章写成一本书,现在我希望你能顺利阅读整个文章。😉
backtests_exponential_smoothing = backtests_multiple_local_estimators(estimator=ExponentialSmoothing())
print(f'overall MAPE: {get_overall_MAPE(backtests_exponential_smoothing)}%')
总体 MAPE: 31.88%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_exponential_smoothing[0].plot(label='Forecast', color='green')
plt.show()
至于 ARIMA,我没有添加季节性信息。现在轮到你了,使用seasonal_periods参数为指数平滑添加这些信息。
Prophet
可视化 Prophet 算法的工作原理,作者提供的图片
Prophet 是 Facebook 在 2017 年的 大规模预测 论文中提出的。它既是一个模型,又是一个同名库。与之前的模型一样,你可以在 Darts 中找到它。该算法是Generalized Additive Model,因此预测是各个组件的总和。这些组件是g(t) — 趋势,s(t) — 季节性(每年、每周和每日),以及h(t) — 假期效应。
y(t) = g(t) + s(t) + h(t) + error(t)
第一个是趋势,它可以随着时间的变化而变化,并且不必始终保持不变。当学生开始学习时间序列分析课程时,他们通常处理的是简单的时间序列。在时间序列中,他们可以看到一个连续增长的趋势。然而,在实际数据中,趋势可能会发生多次变化。Prophet 实现了拐点(可以将其视为超参数,例如它们的数量、范围和先验尺度)。这些点是趋势的变化,例如,增加趋势 -> 拐点 -> 减少趋势 -> 拐点 -> 更强的减少趋势,等等。这种方法更接近我们通常在数据中看到的情况。这些拐点的位置是 Prophet 在你之前设置的。拐点之间的趋势函数可以是简单的回归。
接下来,我们有季节性函数,它是傅里叶级数。
另一个功能是假期效应,它会对我们的预测值进行加减调整。你可以使用 Prophet 库提供的日期,或者定义你自己的事件。你可以想象,黑色星期五效应会显著影响销售额。此外,你可以考虑假期影响预测的日期范围,例如,圣诞节不会影响假期当天的销售,但会影响之前的几天(很多天)。
backtests_prophet = backtests_multiple_local_estimators(estimator=Prophet())
print(f'overall MAPE: {get_overall_MAPE(backtests_prophet)}%')
总体 MAPE:14.38%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_prophet[0].plot(label='Forecast', color='green')
plt.show()
全球模型
现在我们转向一个模型用于所有时间序列的方法。这也被称为跨学习,因为该模型为了对时间序列 A 做出良好的预测,从时间序列 A 以及 B、C、D 等中学习了关系。
监督模型 ~ 时间序列作为回归问题
可视化如何为监督学习模型创建特征,图像来源于作者
现在让我们尝试将监督学习模型应用于时间序列预测。这并不新鲜,但它们往往能给出很好的结果,且比神经网络效果更佳(参见 M-5 竞赛中的最佳解决方案)。然而,回归模型并非专门针对时间序列,因此如果我们想使用它们,就需要将时间序列问题转换为机器学习问题。我在之前的文章中已经详细介绍了这一点,卖出与买入预测中有更多内容,但我也将描述如何使用众所周知的回归算法来解决这个问题。
在我开始之前提到的转换之前,我们首先需要缩放数据。之前我描述了这种转换对于全局模型的必要性。在这种情况下,我使用了MinMaxScaler。
下一步是特征工程,这是一个重复了几次的转换过程。基于时间序列的历史,我们创建有助于模型更好地预测未来的特征。这些变量可以指代所选时间序列的近期历史,例如滞后值(对于每周数据,t-1W,t-2W,t-3W,等等)。另一个例子是滚动统计的计算,如中位数(可以是最近 4 周的中位数)、均值、最小值、最大值、标准差以及你将在窗口上计算的其他内容。如果数据中存在季节性,那么最好给模型一个关于时间点 t 的提示。我经常使用周期变量的正弦和余弦(在上面的可视化中是年份中的一天和一周中的一天)。
最后一步是选择一个模型,你有很多选择,包括线性回归、线性混合效应模型、随机森林、LightGBM 等。模型的选择取决于时间序列的性质和问题的复杂性。另一个问题可能是你想要一个模型还是和预测期数一样多的模型。当你选择一个模型时,需要考虑其弱点。例如,当你选择随机森林时,记住叶子节点计算的是均值,因此它不能超出训练范围。LightGBM 没有这个问题,因为它不计算朴素均值,而是在后台进行回归计算。
现在是时候回到实际操作部分并在代码中实现模型了。我选择了LightGBM作为模型。使用它在 Darts 中比我希望在没有这个库的情况下使用要简单得多。正如你在代码中看到的,我们使用了过去 14 天的滞后数据。我还添加了编码器,添加了模型使用的协变量,这些变量会自动添加,并且所有计算都在内部完成。
-
周期性 — 添加 2 列,基于周期变量(如月份)的正弦余弦编码
-
日期时间属性 — 基于日期时间变量添加标量
-
position — 基于时间序列索引,将相对索引位置添加为整数值,其中 0 设置在预测点。
model_lightGBM = LightGBMModel(lags=14,
output_chunk_length=365,
random_state=0,
multi_models=False,
add_encoders={"cyclic": {"future": ["month"]},
'datetime_attribute': {'future': ['dayofweek']},
'position': {'past': ['relative'], 'future': ['relative']},
'transformer': Scaler()
})
model_lightGBM.fit(series=train_dataset_ts_prepared)
backtests_lightGBM = model_lightGBM.predict(n=forecast_horizons, series=train_dataset_ts_prepared)
# The model predicts scaled values, but then we have to reverse the transformation
backtests_lightGBM = scaler.inverse_transform(backtests_lightGBM) #
print(f'overall MAPE: {get_overall_MAPE(backtests_lightGBM)}%')
总体 MAPE: 15.01%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_lightGBM[0].plot(label='Forecast', color='green')
plt.show()
结果非常有前景,尤其是因为这是一个适用于所有时间序列的模型。然而,基于我的经验,我想提醒你。这些类型的模型在特征工程上表现良好,这既是优势也是大缺陷。假设你使用了滞后和移动平均。现在你将预测一个时间序列的值,但它中有异常值——在预测点之前有几个大值。你的模型肯定会高估。当你创建变量时,尽量想象它们对模型的影响。
DeepAR
基于论文的 arxiv 和模型架构的截图
DeepAR 是由 Amazon 团队开发的一种深度学习算法。它旨在使用递归神经网络(RNNs)对时间序列数据中的复杂依赖关系进行建模。
正如我们在摘要中阅读到的(这与我非常接近):
例如,在零售业务中,预测需求对于在正确的时间和地点提供合适的库存至关重要。
该模型是自回归的,并使用蒙特卡洛样本生成概率预测。神经网络架构基于 LSTM 层。通过概率方法,我们不关心单一的良好预测,而是整个预测分布,来确定真实值可能出现的位置。DeepAR 不是直接使用 LSTMs 进行预测,而是利用 LSTMs 来参数化高斯似然函数。即,估计高斯函数的均值和标准差(θ = (μ, σ) 参数)。
DeepAR 支持未来已知协变量,我们没有这样的数据,但可以创建它们。作为这些特征,我创建了带有星期几和月份的独热编码(OHE)。可能更好的方法是使用正弦和余弦函数,我鼓励你进行实验,并将反馈意见告诉我。
day_series = datetime_attribute_timeseries(
dataset_ts[0], attribute="weekday", one_hot=True, dtype=np.float32
)
month_series = datetime_attribute_timeseries(
dataset_ts[0], attribute="month", one_hot=True, dtype=np.float32
)
day_month_series = day_series.concatenate(month_series, axis=1, ignore_static_covariates=True)
model_deepar = RNNModel(
model="LSTM",
hidden_dim=10,
n_rnn_layers=2,
dropout=0.2,
batch_size=32,
n_epochs=5,
optimizer_kwargs={"lr": 1e-3},
random_state=0,
training_length=21,
input_chunk_length=14,
likelihood=GaussianLikelihood(),
pl_trainer_kwargs={
"accelerator": "gpu",
"devices": [0]
}
)
model_deepar.fit(series=train_dataset_ts_prepared, future_covariates=[train_day_month]*len(train_dataset_ts_prepared), verbose=True)
backtests_deepar = model_deepar.predict(n=forecast_horizons, series=train_dataset_ts_prepared,
future_covariates=[day_month_series]*len(train_dataset_ts_prepared),
num_samples=1000, verbose=True)
backtests_deepar = scaler.inverse_transform(backtests_deepar)
print(f'overall MAPE: {get_overall_MAPE(backtests_deepar)}%')
总体 MAPE: 19.35%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_deepar[0].plot(label='Forecast', color='green')
plt.show()
N-BEATS
基于论文的 arxiv 和模型架构的截图
N-BEATS(用于可解释时间序列预测的神经基础扩展分析)是一种深度学习算法,但它不包含递归层,如 LSTM 或 GRU。
架构可能看起来复杂,但一旦你深入了解细节,它实际上非常简单,是由块组合而成,所有层都是前馈的。
我们从最小的元素——块开始,每个块有一个输入并生成两个输出。输入是回顾期。输出是预测和回溯。我想你对预测的概念很容易理解。回溯是预测,但对于回顾期,它是拟合值,并展示了块在回顾期窗口上的关系好坏。
让我们转到堆栈,或多个块的组合。如你所读,每个块有两个输出和一个输入。接下来的块负责预测残差——类似于提升森林模型中发生的情况,如 AdaBoost。在每一步中,块生成的回溯从前一个块的输入中减去。最后,所有块的预测结果都会聚合。此外,它是一个可解释的模型,你可以分解并查看趋势和季节性的影响。
现在让我们转到组合堆栈。这部分增加了模型的深度,并提供了更多了解复杂性的机会。
model_nbeats = NBEATSModel(
input_chunk_length=178,
output_chunk_length=356,
generic_architecture=False,
num_blocks=3,
num_layers=4,
layer_widths=512,
n_epochs=10,
nr_epochs_val_period=1,
batch_size=800,
model_name="nbeats_interpretable_run",
random_state=0,
pl_trainer_kwargs={
"accelerator": "gpu",
"devices": [0]
}
)
model_nbeats.fit(series=train_dataset_ts_prepared, verbose=True)
backtests_nbeats = model_nbeats.predict(n=forecast_horizons, series=train_dataset_ts_prepared, verbose=True)
backtests_nbeats = scaler.inverse_transform(backtests_nbeats)
print(f'overall MAPE: {get_overall_MAPE(backtests_nbeats)}%')
整体 MAPE:13.18%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_nbeats[0].plot(label='Forecast', color='green')
plt.show()
TFT
arxiv 和模型架构的截图,基于论文
时间融合变换器(TFT)是谷歌开发的用于时间序列预测的深度学习算法。它旨在通过结合变换器网络和自回归建模来建模时间序列数据中的复杂依赖关系和关系。
TFT 是最复杂的架构,使用了各种底层技术。它像洋葱一样,由多层组成。此外,根据我的经验,它相比上述模型学习时间最长。TFT 使用多头注意力块来寻找长期模式,但使用 LSTM 序列到序列编码器/解码器来寻找这些较短的模式。
model_tft = TFTModel(
input_chunk_length=28,
output_chunk_length=356,
hidden_size=16,
lstm_layers=1,
num_attention_heads=3,
dropout=0.1,
batch_size=32,
n_epochs=5,
add_encoders={"cyclic": {"future": ["month"]},
'datetime_attribute': {'future': ['dayofweek']},
'position': {'past': ['relative'], 'future': ['relative']},
'transformer': Scaler()
},
add_relative_index=False,
optimizer_kwargs={"lr": 1e-3},
random_state=0,
pl_trainer_kwargs={
"accelerator": "gpu",
"devices": [0]
}
)
model_tft.fit(series=train_dataset_ts_prepared, verbose=True)
backtests_tft = model_tft.predict(n=forecast_horizons, series=train_dataset_ts_prepared,
num_samples=1000, verbose=True)
backtests_tft = scaler.inverse_transform(backtests_tft)
print(f'overall MAPE: {get_overall_MAPE(backtests_tft)}%')
整体 MAPE:13.37%
fig, ax = plt.subplots(figsize=(30, 10))
test_dataset_ts[0].plot(label='True value', color='black')
backtests_tft[0].plot(label='Forecast', color='green')
plt.show()
总结
在这篇文章中,我想向你展示你可以选择哪些方法来预测多个时间序列。我提供了完全实用的代码,随意使用它们,不要犹豫与我联系。
这只是对主题的介绍。我认为数据科学家在供应链公司工作的相关主题还包括以下内容:
-
层级预测及随后合并来自不同层级的预测,即层级调整。我们可以在商店级别进行预测,也可以在国家级别进行预测,但当我们将商店级别的预测聚合时,作为总和,我们希望得到与国家级别预测显示的相同结果。这就是层级调整重要的原因。
-
另一个话题是 库存优化,即我们应该在库存中拥有多少产品,以避免没有产品库存的情况,但另一方面,我们也不希望某一产品库存数月。
有问题吗?
问题,图片由作者提供
我意识到在这篇文章中我触及了许多主题。我想给你一个可以采取的方向的指示。也许其中一些应该在这里更详细地描述,而其他的则在新文章中详细描述。请不要犹豫,你可以在 Linkedin 找到我。在未来的文章中,我希望它们能够涵盖详细的主题,并展示如何使用 PyTorch 库从头开始实现时间序列模型。
感谢你的时间!
数据集来源:
杂项{仅限需求预测内核,
作者 = {inversion},
标题 = {商店商品需求预测挑战},
出版商 = {Kaggle},
年份 = {2018},
网址 = {https://kaggle.com/competitions/demand-forecasting-kernels-only},
许可证 = CC
}
预测 API:一个使用 Django 和 Google Trends 的示例
原文:
towardsdatascience.com/forecasting-api-an-example-with-django-and-google-trends-9b55046bd578
构建一个 web 应用程序以预测 Google Trends 的发展趋势。
·发布于 Towards Data Science ·阅读时间 14 分钟·2023 年 8 月 1 日
--
“Google Trends”,由 Giulia Roggia。经许可使用。
-
简介
-
Django 模型
-
服务:数据源,预处理,机器学习,任务
-
交互层:序列化器,视图,端点
-
结论
简介
什么是 Django?
Django 是一个高级 Python 网络框架。它设计得快速、安全、可扩展,因此是开发预期会增长复杂性的强大 web 应用程序的热门选择。有关 Django 的介绍,请参阅 这个教程。
在这个示例中,我们将使用 Django Rest Framework(DRF),这是 Django 的一个扩展,旨在简化 REST API 的开发。有关 DRF 的介绍,请参阅 这个教程。
需求
我们将通过列出一些假设的需求来开始设计我们的应用:
-
总体目标:实现一个系统来预测未来时间序列的值。
-
数据: Google Trends 的周频数据,包含特征和目标,未来可能会扩展。数据应根据需求下载。
-
预处理:仅使用滞后值。
-
机器学习模型:一个全球 LightGBM 模型(如果你想了解更多关于全球与本地模型的区别,可以查看 这篇文章)。
-
推断:生成在线预测(与批量预测相对),但不需要提供输入特征。
本教程中使用的完整代码可在 这里获取。
设置环境
让我们从列出所需的依赖项开始。
python = "³.8"
Django = "⁴.2.1"
lightgbm = "³.3.5"
pandas = "².0.1"
djangorestframework = "³.14.0"
pytrends = "⁴.9.2"
drf-extensions = "⁰.7.1"
我们将使用poetry来管理依赖项,使用Docker来容器化项目。你可以在这里查看本项目中使用的 poetry 和 docker 文件。
快速开始
如果你想跳过前面的内容,直接开始使用应用程序,你可以运行以下命令:
# Clone the project.
git clone git@github.com:davide-burba/code-collection.git
# Move to the right folder.
cd code-collection/examples/api-example-django
# Launch the app.
docker compose up -d
# Apply the migrations.
docker compose exec django ./manage.py migrate
# Interactively create a (super)user for your app.
docker compose exec django ./manage.py createsuperuser
并连接到localhost:8000/gtrends
!
Django 模型
在本节中,我们列出了示例中使用的 Django 模型。
时间序列
对于一个预测系统,我们需要处理时间序列数据。通常我们只需要两个模型:一个用于识别每个时间序列,另一个用于存储其值。但由于Google Trends 的历史数据可能会因为归一化而每天发生变化,我们还需要对数据进行版本控制,这会导致额外的一个模型。
我们还创建了一个模型,用于列出不同的数据源(目前只有 Google Trends)。
class TimeSeries(models.Model):
name = models.CharField(unique=True, max_length=64)
source = models.CharField(max_length=32, choices=DataSource.choices)
class TSVersion(models.Model):
timeseries = models.ForeignKey(TimeSeries, on_delete=CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
expired = models.BooleanField(default=False)
class TSValue(models.Model):
version = models.ForeignKey(TSVersion, on_delete=CASCADE)
time = models.DateTimeField()
value = models.FloatField()
class DataSource(models.TextChoices):
GOOGLE_TRENDS = "GOOGLE_TRENDS"
配置
要训练一个监督模型,我们需要一组特征和目标。我们可以将这些信息存储在一个“数据配置”中。这个过程通过以下模型完成。
class DataConfig(models.Model):
name = models.CharField(unique=True, max_length=64)
class DataFeatures(models.Model):
config = models.ForeignKey(
DataConfig, on_delete=CASCADE, related_name="features"
)
timeseries = models.ForeignKey(TimeSeries, on_delete=PROTECT)
class DataTargets(models.Model):
config = models.ForeignKey(
DataConfig, on_delete=CASCADE, related_name="targets"
)
timeseries = models.ForeignKey(TimeSeries, on_delete=PROTECT)
同样,我们需要存储预处理和机器学习(ML)模型的配置。为了简化,我们将这些存储在一个JSONField
中:
class PreprocessingConfig(models.Model):
name = models.CharField(unique=True, max_length=64)
params = models.JSONField()
class MLConfig(models.Model):
params = models.JSONField()
ML 模型
在这里,我们区分了 ML 配置和 ML 模型。ML 配置包含关于 LightGBM 参数的数据,而 ML 模型包含所有关于数据、预处理和 ML 配置的信息。
一个 ML 模型可以在不同的数据集上进行估计(由于新数据的到来),因此我们还需要对 ML 模型进行版本控制。这会导致以下两个模型:
class MLModel(models.Model):
name = models.CharField(unique=True, max_length=64)
ml_config = models.ForeignKey(MLConfig, on_delete=PROTECT)
data_config = models.ForeignKey(DataConfig, on_delete=PROTECT)
preprocess_config = models.ForeignKey(
PreprocessingConfig, on_delete=PROTECT
)
class MLModelVersion(models.Model):
ml_model = models.ForeignKey(MLModel, on_delete=CASCADE)
ml_file = models.FileField(upload_to="ml_models")
created_at = models.DateTimeField(auto_now_add=True)
metadata = models.JSONField()
MLModelVersion
保存了指向 LightGBM 工件的链接在ml_file
中,以及关于它所训练数据的信息在metadata
中。工件存储在 django 设置模块中指定的位置:例如,可能是文件系统中的一个文件夹或云中的一个 S3 桶。
服务
在本节中,我们描述了包含应用逻辑的服务。根据Django-StyleGuide,将其与视图分开是最佳实践。
数据源
由于数据源预计将来会增长,我们将使用灵活的设计:
-
一个抽象的
DataSource
类,定义了一个接口。 -
一个
GTrendSource
类,继承自DataSource
并实现下载 Google Trends 数据的细节。 -
一个
download_data
工厂用来构建DataSource
子类。
这会产生以下模块:
import datetime as dt
from abc import ABC, abstractmethod
import pandas as pd
from gtrends.models import TimeSeries
from pytrends.request import TrendReq
def download_data(timeseries: TimeSeries) -> pd.DataFrame:
return DATASOURCE_MAPtimeseries.source.download()
class DataSource(ABC):
def __init__(self, timeseries: TimeSeries):
self.timeseries = timeseries
@abstractmethod
def download(self) -> pd.DataFrame:
"""Returns a dataframe with time as index and value as column."""
class GTrendSource(DataSource):
START_DATE = "2022-01-01"
def download(self) -> pd.DataFrame:
# Download data.
name = self.timeseries.name
data = self.download_interest_over_time(name)
# Format new data.
data = (
data[~data.isPartial]
.reset_index()
.rename(columns={name: "value", "date": "time"})
.set_index("time")
)[["value"]]
data["value"] = data["value"].astype(float)
return data
@classmethod
def download_interest_over_time(cls, search_term: str) -> pd.DataFrame:
"""Download Google Trends data."""
pytrends = TrendReq()
timeframe = (
cls.START_DATE + " " + dt.datetime.now().strftime("%Y-%m-%d")
)
pytrends.build_payload([search_term], timeframe=timeframe)
return pytrends.interest_over_time()
DATASOURCE_MAP = {
"GOOGLE_TRENDS": GTrendSource,
}
预处理
一旦数据加载完成,我们希望预处理数据以便后续训练模型或推断未来值。让我们从定义一个接口类开始。
class BasePreprocessor(ABC):
@abstractmethod
def build_x_y(self, data: Dict) -> Tuple[pd.DataFrame, pd.Series]:
"""Return features and target ready for training."""
@abstractmethod
def build_x_latest(self, data: Dict) -> pd.DataFrame:
"""Return only latest values of features, useful for inference"""
目前我们只考虑用于特征工程的滞后值。让我们创建一个实现滞后逻辑的辅助函数。
def _build_lags(
df: pd.DataFrame, column: str, lags: List[int], prefix: str
) -> pd.DataFrame:
return pd.concat(
[
df[[column]]
.shift(lag)
.rename(columns={column: f"{prefix}_lag_{lag}"})
for lag in lags
],
axis=1,
)
现在我们需要创建一个实现基类抽象方法的类。让我们首先定义它的属性。
@dataclass
class Preprocessor(BasePreprocessor):
horizon: int
target_lags: List[int]
feature_lags: List[int]
请注意,目标本身可以作为特征使用。对于作为特征使用的目标,我们分配一个通用前缀“target”。当有多个目标时,这可能是有益的,因为它们将堆叠在相同的列中,从而减少特征的数量。
这里是构建滞后目标和特征的实现:
def _build_x_lags_targets(
self, target_data: Dict
) -> Optional[pd.DataFrame]:
if not self.target_lags:
return None
x = []
for df in target_data.values():
x.append(
_build_lags(
df=df,
column="value",
lags=self.target_lags,
prefix="target",
)
)
return pd.concat(x, axis=1)
def _build_x_lags_features(
self, feature_data: Dict, target_data: Dict
) -> Optional[pd.DataFrame]:
if not self.feature_lags:
return None
x = []
for name, df in feature_data.items():
x.append(
_build_lags(
df=df,
column="value",
lags=self.feature_lags,
prefix=name,
)
)
# Concat features on axis 1.
x = pd.concat(
[df.reset_index().drop(columns=["ts_name"]) for df in x], axis=1
)
# Use target to "reindex" on axis 0.
for_reindex = pd.concat(target_data.values(), axis=1).reset_index()
x = pd.merge(for_reindex, x, how="left", on="time")
return x.drop(columns=["value"]).set_index(["time", "ts_name"])
现在我们可以将它们封装在一个build_x
方法中:
def build_x(self, data: Dict) -> pd.DataFrame:
target_data = data["targets"]
feature_data = data["features"]
# Build x_target and x_features.
x_targ = self._build_x_lags_targets(target_data)
x_feat = self._build_x_lags_features(feature_data, target_data)
# Combine x_target and x_features.
if x_feat is None and x_targ is None:
raise ValueError("Cannot have no target lags and no feature lags.")
elif x_feat is None:
return x_targ
elif x_targ is None:
return x_feat
return pd.merge(
x_targ, x_feat, left_index=True, right_index=True, how="left"
)
为了构建目标,我们只需将其移动horizon
时间步:
def build_y(self, target_data: Dict) -> pd.DataFrame:
y = {}
for name, df in target_data.items():
y[name] = (
df["value"]
.shift(-self.horizon)
.rename(f"horizon_{self.horizon}")
)
return pd.concat(y.values())
现在我们可以为所需的抽象方法提供实现:
def build_x_y(self, data: Dict) -> Tuple[pd.DataFrame, pd.Series]:
# Build x and y.
x = self.build_x(data)
y = self.build_y(data["targets"])
# Align x indexes with y indexes.
x = pd.merge(y, x, left_index=True, right_index=True, how="left")
x = x.iloc[:, 1:]
# Drop missing values generated by lags/horizon.
idx = ~(x.isnull().any(axis=1) | y.isnull())
x = x.loc[idx]
y = y.loc[idx]
return x, y
def build_x_latest(self, data: Dict) -> pd.DataFrame:
x = self.build_x(data)
return x[x.index == x.index.max()]
ML
让我们定义一个非常简单的 ML 模块,提供两个函数用于转储和加载 LightGBM 模型(如果你想知道我们为何选择使用 LightGBM,你可以查看这篇文章)。
import lightgbm
def save_engine(model, path):
model.booster_.save_model(path)
def load_engine(path):
return lightgbm.Booster(model_file=path)
任务
在定义 API 接口之前,让我们定义一些将在端点中使用的tasks
。
更新时间序列
我们首先需要存储一些时间序列数据。我们不希望每次下载一个新的数据点时都增加一个新版本到数据库中,因为这样会快速增长并产生大量重复数据。相反,我们希望将新下载的数据与最新版本进行比较,只有当数据历史不匹配时才创建新版本。这是在以下代码片段中完成的:
from typing import Tuple
import pandas as pd
from gtrends.services.data_sources import download_data
from gtrends.models import TimeSeries, TSValue, TSVersion
def update_timeseries(timeseries: TimeSeries) -> Tuple[bool, int]:
"""Update timeseries values.
Either add the new values (if past values are the same) to the latest
version, or create a new version.
Args:
timeseries: A timeseries object.
Returns:
A pair with:
- bool: True if it created a new version, False otherwise
- int: The number of new values added.
"""
new_data = download_data(timeseries)
# Assign version.
new_version = True
versions = timeseries.tsversion_set.order_by("created_at")
if versions:
version = versions.last()
old_data = _build_old_data(version)
if _is_old_data_in_new_data(old_data, new_data):
# If old values match, just keep the new values.
new_version = False
new_data = new_data.loc[~new_data.index.isin(old_data.index)]
else:
# Else, set the old version to expired.
version.expired = True
version.save()
if new_version:
version = TSVersion(timeseries=timeseries)
version.save()
# Store new data.
objs = [
TSValue(version=version, time=d[0], value=d[1].value)
for d in new_data.iterrows()
]
TSValue.objects.bulk_create(objs)
return new_version, len(objs)
def _build_old_data(version: TSVersion) -> pd.DataFrame:
old_data = pd.DataFrame(
version.tsvalue_set.values("time", "value")
).set_index("time")
old_data.index = old_data.index.tz_localize(None)
return old_data
def _is_old_data_in_new_data(
old_data: pd.DataFrame, new_data: pd.DataFrame
) -> bool:
if old_data.index.isin(new_data.index).all() and new_data.loc[
old_data.index
].equals(old_data):
return True
return False
我们还可以添加一个包装器,以方便地更新数据库中的所有时间序列:
def update_all_timeseries():
"""Update all time-series values."""
for ts in TimeSeries.object.all():
update_timeseries(ts)
加载数据
现在我们已经存储了一些时间序列数据,我们希望能够加载它以便进行预处理。
from typing import Dict, List
import pandas as pd
from gtrends.models import TimeSeries
def load_data(
target_ts: List[TimeSeries], feature_ts: List[TimeSeries]
) -> Dict[str, Dict[str, pd.DataFrame]]:
targ_feat = {
"targets": target_ts,
"features": feature_ts,
}
data = {"targets": {}, "features": {}}
metadata = {"targets": {}, "features": {}}
for key, items in targ_feat.items():
for item in items:
ts = item.timeseries
version = ts.tsversion_set.last()
values = version.tsvalue_set.all().values("time", "value")
df = pd.DataFrame(values)
df["ts_name"] = ts.name
df = df.set_index(["time", "ts_name"])
data[key][ts.name] = df
metadata[key][ts.name] = version.id
return data, metadata
预处理
让我们将预处理封装成几个任务。为了简化,我们直接使用Preprocessor
类;如果以后想添加另一个预处理类,我们可以使用与数据源相同的工厂模式。
def preprocess(data: Dict, prep_params: Dict) -> Tuple[pd.DataFrame, pd.Series]:
return Preprocessor(**prep_params).build_x_y(data)
def build_x_latest(data: Dict, prep_params: Dict) -> pd.DataFrame:
return Preprocessor(**prep_params).build_x_latest(data)
训练
现在我们已经有了预处理的数据,我们终于可以训练我们的 LightGBM 模型了。请注意,这是更简单的步骤!
from lightgbm import LGBMRegressor
def train(x, y, model_params):
model = LGBMRegressor(**model_params)
model.fit(x, y)
return model
存储 ML 模型
现在我们可以创建一个任务,将 LightGBM 引擎保存以创建一个新的MLModelVersion
。注意,我们首先将引擎转储到一个临时文件中。这只是一个技巧,以避免在任务中硬编码存储类型,而是通过 Django 设置动态处理。有关此主题的更多信息,请查看FileField 文档。
import os
from typing import Dict
from uuid import uuid4
from django.core.files import File
from gtrends import models
from gtrends.ml import save_engine
from lightgbm import LGBMRegressor
def save_mlmodelversion(
engine: LGBMRegressor, ml_model: models.MLModel, metadata: Dict
) -> models.MLModelVersion:
filename = str(uuid4()).replace("-", "")[:10] + ".txt"
tmp_path = "/tmp/" + filename
save_engine(engine, tmp_path)
with open(tmp_path, "r") as f:
ml_model_file = File(f, filename)
ml_model_version = models.MLModelVersion(
ml_model=ml_model,
ml_file=ml_model_file,
metadata=metadata,
)
ml_model_version.save()
os.remove(tmp_path)
return ml_model_version
管道
我们现在可以完成训练和推断管道。由于这些管道速度较快,可以将它们简单地封装在一个函数中。然而,对于长时间运行的管道或高流量的情况,建议使用任务管理器,如 AirFlow 或 Celery。
这是训练管道:
from gtrends.models import MLModel, MLModelVersion
from gtrends.services.tasks import (
load_data,
preprocess,
save_mlmodelversion,
train,
)
def train_pipeline(ml_model: MLModel) -> MLModelVersion:
target_ts = ml_model.data_config.targets.all()
feature_ts = ml_model.data_config.features.all()
prep_params = ml_model.preprocess_config.params
model_params = ml_model.ml_config.params
data, metadata = load_data(target_ts, feature_ts)
x, y = preprocess(data, prep_params)
engine = train(x, y, model_params)
ml_model_version = save_mlmodelversion(engine, ml_model, metadata)
return ml_model_version
这是推断管道:
from typing import Dict
from gtrends.models import MLModel
from gtrends.services.ml import load_engine
from gtrends.services.tasks import build_x_latest, load_data
def inference_pipeline(ml_model: MLModel) -> Dict:
if ml_model.mlmodelversion_set.count() == 0:
raise ValueError("No model has been trained yet!")
target_ts = ml_model.data_config.targets.all()
feature_ts = ml_model.data_config.features.all()
prep_params = ml_model.preprocess_config.params
data, _ = load_data(target_ts, feature_ts)
x = build_x_latest(data, prep_params)
engine = load_engine(ml_model.mlmodelversion_set.last().ml_file.path)
y_pred = engine.predict(x)
# Format predictions.
horizon = ml_model.preprocess_config.params["horizon"]
preds = {
name: {"last_date": time, "prediction": pred, "horizon": horizon}
for time, name, pred in zip(
x.index.get_level_values("time"),
x.index.get_level_values("ts_name"),
y_pred,
)
}
for k, v in preds.items():
last_value = data["targets"][k].loc[v["last_date"]]["value"].item()
preds[k]["last_value"] = last_value
preds[k]["predicted_delta"] = v["prediction"] - last_value
return preds
交互层
序列化器、视图和端点代表应用程序的“交互”层,这是最外部的层,并定义了用户如何与应用程序交互。
序列化器
序列化器负责序列化和反序列化数据。以下是一个简单的示例:
class TSVersionSerializer(serializers.ModelSerializer):
class Meta:
model = models.TSVersion
fields = "__all__"
为了简洁起见,我们只讨论那些非平凡的视图。
时间序列
对于时间序列,我们重写默认的创建方法,以便在创建 TimeSeries
对象时下载第一个版本。
class TimeSeriesSerializer(serializers.ModelSerializer):
class Meta:
model = models.TimeSeries
fields = "__all__"
def create(self, validated_data):
ts = super().create(validated_data)
# Download data for the newly created timeseries.
update_timeseries(ts)
return ts
数据配置
对于数据配置,我们添加了特征和目标的链接,以便以后从单个端点创建数据配置。
class DataConfigSerializer(serializers.ModelSerializer):
features = DataFeaturesSerializer(many=True)
targets = DataTargetsSerializer(many=True, allow_null=False)
class Meta:
model = models.DataConfig
fields = ("id", "name", "features", "targets")
def validate_targets(self, value):
if len(value) == 0:
raise serializers.ValidationError("Must have at least one target.")
return value
def create(self, validated_data):
config = models.DataConfig.objects.create(**validated_data)
features_data = validated_data.pop("features")
targets_data = validated_data.pop("targets")
for feature_data in features_data:
models.DataFeatures.objects.create(config=config, **feature_data)
for target_data in targets_data:
models.DataTargets.objects.create(config=config, **target_data)
return config
预处理
让我们在预处理配置上添加一个验证步骤,以检查是否具有预期的参数:
class PreprocessingConfigSerializer(serializers.ModelSerializer):
class Meta:
model = models.PreprocessingConfig
fields = "__all__"
def validate_params(self, value):
expected_keys = {"horizon", "target_lags", "feature_lags"}
if set(value.keys()) != expected_keys:
raise serializers.ValidationError(f"Expected keys: {expected_keys}")
if not isinstance(value["horizon"], int):
raise serializers.ValidationError("Horizon must be an int.")
for key in ["target_lags", "feature_lags"]:
if not isinstance(value[key], list) or not all(
isinstance(lag, int) for lag in value[key]
):
raise serializers.ValidationError(f"{key} must be a list[int].")
return value
ML 配置
同样,让我们检查为 ML 配置提供的参数是否有效:
class MLConfigSerializer(serializers.ModelSerializer):
class Meta:
model = models.MLConfig
fields = "__all__"
def validate_params(self, value):
# At the moment we only support LightGBM model.
valid_params = LGBMRegressor().get_params()
invalid_params = [k for k in value if k not in valid_params]
if invalid_params:
raise serializers.ValidationError(f"Invalid: {invalid_params}")
return value
视图
现在让我们定义用户如何与 API 交互。我们将定义一些动作来触发数据检索或 ML 模型训练。同样,这种做法是可以接受的,因为这些动作执行速度很快,并且我们希望在项目开始时保持简单(如果你对这种哲学感兴趣,可以查看 YAGNI)。然而,随着项目的增长,可能需要将这些动作异步化并使用任务管理器。
请注意,为了保持版本控制,我们不允许使用 put
和 patch
方法。
时间序列
让我们为时间序列建立一个视图。我们希望能够:
-
创建时间序列并列出它们
-
获取最新版本的值
-
更新值
class TimeSeriesViewSet(viewsets.ModelViewSet):
queryset = models.TimeSeries.objects.prefetch_related("tsversion_set").all()
serializer_class = serializers.TimeSeriesSerializer
http_method_names = ["get", "post", "head", "delete"]
@action(detail=True, url_path="latest-values")
def latest_values(self, request, pk):
"""Get timeseries values for the last version."""
versions = self.queryset.get(pk=pk).tsversion_set.order_by("created_at")
if not versions:
return Response([])
return Response(versions.first().tsvalue_set.values("time", "value"))
@action(detail=True, url_path="update-values")
@transaction.atomic
def update_values(self, request, pk):
"""Update values for one timeseries."""
timeseries = self.queryset.get(pk=pk)
try:
new_version, how_many = update_timeseries(timeseries)
except ResponseError:
return Response(
"Data download failed!", status=status.HTTP_400_BAD_REQUEST
)
msg = f"Data updated, {how_many} new values. "
if new_version:
msg += " New version created."
else:
msg += " Updated latest version."
return Response(msg)
@action(detail=False, url_path="update-all-values")
@transaction.atomic
def update_all_values(self, request):
"""Update values for all timeseries."""
try:
for timeseries in self.queryset:
update_timeseries(timeseries)
except ResponseError:
return Response(
"Data download failed!", status=status.HTTP_400_BAD_REQUEST
)
msg = f"Data updated, {len(self.queryset)} timeseries updated."
return Response(msg)
我们还希望列出一个时间序列的不同版本,并检查其值。
class TSVersionViewSet(viewsets.ModelViewSet):
queryset = models.TSVersion.objects.prefetch_related("tsvalue_set").all()
serializer_class = serializers.TSVersionSerializer
http_method_names = ["get", "head"]
@action(detail=True)
def values(self, request, pk, **kwargs):
"""Get timeseries values."""
values = self.queryset.get(pk=pk).tsvalue_set.values("time", "value")
return Response(values)
配置
我们希望列出、添加和删除配置。它们的视图是直接的。
class MLConfigViewSet(viewsets.ModelViewSet):
queryset = models.MLConfig.objects.all()
serializer_class = serializers.MLConfigSerializer
http_method_names = ["get", "post", "head", "delete"]
class DataConfigViewSet(viewsets.ModelViewSet):
queryset = models.DataConfig.objects.all()
serializer_class = serializers.DataConfigSerializer
http_method_names = ["get", "post", "head", "delete"]
class PreprocessingConfigViewSet(viewsets.ModelViewSet):
queryset = models.PreprocessingConfig.objects.all()
serializer_class = serializers.PreprocessingConfigSerializer
http_method_names = ["get", "post", "head", "delete"]
ML 模型
对于 ML 模型,我们希望能够创建和列出它们,并执行训练和推断。
class MLModelViewSet(viewsets.ModelViewSet):
queryset = (
models.MLModel.objects.select_related("preprocess_config", "ml_config")
.prefetch_related(
"data_config__targets",
"data_config__features",
"mlmodelversion_set",
)
.all()
)
serializer_class = serializers.MLModelSerializer
http_method_names = ["get", "post", "head", "delete"]
@action(detail=True)
def train(self, request, pk, **kwargs):
ml_model = self.queryset.get(pk=pk)
ml_model_version = train_pipeline(ml_model)
return Response(
serializers.MLModelVersionSerializer(ml_model_version).data
)
@action(detail=True)
def predict(self, request, pk, **kwargs):
ml_model = self.queryset.get(pk=pk)
if ml_model.mlmodelversion_set.count() == 0:
return Response(
"No model has been trained yet!",
status=status.HTTP_404_NOT_FOUND,
)
predictions = inference_pipeline(ml_model)
return Response(predictions)
我们还希望列出特定 MLModel
的所有版本。
class MLModelVersionViewSet(viewsets.ModelViewSet):
queryset = models.MLModelVersion.objects.all()
serializer_class = serializers.MLModelVersionSerializer
http_method_names = ["get", "head"]
端点
最后,我们可以将刚创建的视图链接到一组端点:
-
timeseries
用于时间序列 -
timeseries/<id>/versions
用于时间序列版本 -
model-config
、data-config
、preprocessing-config
用于配置对象 -
model
用于 ML 模型 -
model/<id>/versions
用于 ML 模型版本
这在以下模块中完成:
from django.urls import include, path
from gtrends import views
from rest_framework_extensions.routers import ExtendedDefaultRouter
router = ExtendedDefaultRouter()
router.register("model-config", views.MLConfigViewSet)
router.register("data-config", views.DataConfigViewSet)
router.register("preprocessing-config", views.PreprocessingConfigViewSet)
models = router.register("model", views.MLModelViewSet)
models.register(
"versions",
views.MLModelVersionViewSet,
basename="version",
parents_query_lookups="model_id",
)
timeseries = router.register("timeseries", views.TimeSeriesViewSet)
timeseries.register(
"versions",
views.TSVersionViewSet,
basename="version",
parents_query_lookups="timeseries_id",
)
urlpatterns = [
path("", include(router.urls)),
]
请注意,我们不需要为数据检索或训练定义端点,因为这些会自动添加为“额外操作”。例如,训练模型的端点是 model/<id>/train
。
结论
我们在 Django 中看到了一个预测系统的示例实现。你现在可以开始玩玩这个系统了!要开始,请按照文章开头的说明进行操作,以便你可以连接到 localhost:8000/gtrends
并玩转这个应用。
完成设置并训练好模型后,你应该会看到类似于这样的结果(这里我们预测的是术语 "forecasting"):
当你预测未来值时,你应该会看到类似这样的结果:
就这些,你现在可以根据需要实现不同的变体!
这里是可能改进的非详尽列表:
喜欢这篇文章吗? 查看我的其他文章 并关注我以获取更多内容! 点击这里 以无限阅读文章,并且对你没有额外费用的情况下支持我❤️
用 Python 预测间歇时间序列
原文:
towardsdatascience.com/forecasting-intermittent-time-series-in-python-9fd028a0c9ee
Python 中间歇时间序列预测的完整指南及其重点项目
·发表于Towards Data Science ·15 分钟阅读·2023 年 8 月 7 日
--
间歇时间序列或稀疏时间序列是一种特殊情况,其中非零值在时间上偶尔出现,而其余的值都是 0。
一个常见的稀疏时间序列示例是降雨量随时间的变化。可能会有连续很多天没有降雨,而降雨时,降雨量会有所不同。
另一个实际的间歇序列示例是对缓慢移动或高价值物品的需求,例如航空航天或重型机械中的备件。
一些时间序列的间歇性特征在预测中带来了真正的挑战,因为传统模型对间歇性处理不佳。因此,我们必须转向为稀疏时间序列量身定制的替代预测方法。
在这篇文章中,我们将探讨预测间歇时间序列的不同方法。和往常一样,我们首先从理论上探讨每个模型,然后在 Python 中实现它们。
和往常一样,完整的源代码可在GitHub上获得。
通过我的 免费时间序列备忘单 在 Python 中学习最新的时间序列分析技术!获取统计和深度学习技术的实现,全部使用 Python 和 TensorFlow!
让我们开始吧!
Croston 方法
Croston 方法是预测稀疏时间序列最常见的方法之一。它通常作为基准模型来评估更复杂的方法。
使用 Croston 方法,从原始序列中构建两个序列:
-
包含只有零值的时间序列
-
包含非零值时间段的时间序列
让我们考虑一个示例来说明这一点。考虑下面的稀疏时间序列:
一个模拟的稀疏时间序列。图片由作者提供。
然后,根据 Croston 方法,我们创建两个新的序列:一个包含非零值,另一个包含分隔非零值的时间周期。
构建两个新的序列。第一个序列只有非零值。第二个序列是非零值之间的周期。请注意,我们在两个连续值之间分配了周期为 1。图片由作者提供。
从上表中,我们可以看到我们将非零值表示为 qᵢ,两个连续非零值之间的周期表示为 aᵢ。
还要注意 aᵢ 的第一个值是 1,因为我们在 t=1 处有一个非零值。此外,两个连续值之间的周期也被认为是 1。
从那里,我们使用简单指数平滑根据下面的方程预测每个序列:
预测非零值。图片由作者提供。
预测连续非零值之间的时间间隔。图片由作者提供。
当然,平滑参数 alpha 在 0 和 1 之间,因为我们使用的是简单指数平滑。请注意,两条方程中使用的是相同的平滑参数。
然后,最终预测是 q 和 a 的比率,如下方的方程所示:
使用 Croston 方法的一步预测。图片由作者提供。
现在,由于使用简单指数平滑来预测每个序列,预测将是一个平坦的水平线。这就是为什么我们通常将其作为基线模型的原因。
此外,大多数基本 Croston 方法的实现使用平滑参数值为 0.1。
再次强调,这是预测间歇性时间序列的最基本方法,但有办法轻松改进它,我们将在接下来讨论。
改进 Croston 方法
正如我们之前看到的,经典的 Croston 方法使用相同的平滑参数 0.1 来预测两个构建的序列,这似乎并不理想。
提出了 Croston 方法的优化版本,其中平滑参数在 0.1 和 0.3 之间变化。此外,每个序列都是单独优化的。
一切保持不变,但现在我们为每个序列都有唯一的优化平滑参数,这些参数构成了最终预测。
Croston 方法的应用
让我们在一个模拟数据集上实现 Croston 方法,看看我们可以用这个模型做出什么样的预测。
首先,我将导入所需的库并读取数据。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
sim_df = pd.read_csv('intermittent_time_series.csv')
然后,我们使用 statsforecast 中的实现。现在,让我们使用经典版本的 Croston 方法,该方法使用平滑因子 0.1。
from statsforecast import StatsForecast
from statsforecast.models import CrostonClassic
models = [CrostonClassic()]
sf = StatsForecast(
df=sim_df,
models=models,
freq='H',
n_jobs=-1
)
然后,为了将模型的预测与我们模拟数据集中的实际数据进行比较,我们运行交叉验证函数。在这里,我们将预测范围设置为 1,因此我们的预测曲线会在数据集的最后 50 个时间步上每个时间步进行更新。
cv_df = sf.cross_validation(
df=sim_df,
h=1,
step_size=1,
n_windows=50
)
然后,我们可以绘制实际值和模型预测值。
fig, ax = plt.subplots(figsize=(10,8))
ax.bar(sim_df.index, sim_df['y'], color='lightgray')
ax.plot(cv_df.index, cv_df['CrostonClassic'], ls='--', label='Croston')
ax.set_ylabel('Value')
ax.set_xlabel('Time steps')
ax.legend(loc='best')
plt.xlim(40, 100)
plt.tight_layout()
使用 Croston 方法预测下一个时间步。图片由作者提供。
从上图中,我们可以直观地看到,Croston 方法实际上是间歇时间序列的加权平均。
仔细观察,如果过去的值较大,那么下一个预测值会增加;如果过去的值较小,那么下一个预测值会减少。
还要注意一段时间内我们有连续的零值,这意味着预测曲线没有更新,保持平坦。
最后,请记住我们的预测曲线变化很大,因为我们只预测下一个时间步。如果我们设定一个更长的预测范围,曲线将更像一个楼梯,因为 Croston 方法输出的是一个恒定值。
优化的 Croston 方法在实际操作中
现在,让我们重复上面的练习,但使用优化版本的 Croston 方法,其中平滑参数分别针对非零值序列和零值序列进行优化。
from statsforecast.models import CrostonOptimized
models = [CrostonOptimized()]
sf = StatsForecast(
df=sim_df,
models=models,
freq='H',
n_jobs=-1
)
cv_df = sf.cross_validation(
df=sim_df,
h=1,
step_size=1,
n_windows=50
)
cv_df.index = np.arange(50, 100, 1)
绘制结果生成了下图。
使用优化后的 Croston 方法进行预测。在这种情况下,它与之前的方法基本相同。图片由作者提供。
从图中可以看到,优化平滑参数导致的预测与经典方法对我们模拟数据的预测几乎相同。
现在我们了解了 Croston 方法,让我们继续学习另一种预测技术。
汇总-拆分间歇性需求方法(ADIDA)
汇总-拆分间歇性需求方法(ADIDA)旨在通过在较低频率下汇总序列来去除间歇性。
例如,如果每小时数据有零值,那么对 24 小时数据求和以获得每日数据可能会去掉零值。同样的逻辑也适用于间歇性的日数据,我们可以将其汇总为周数据,以去除零值期间。
用五个时间步的滚动总和来汇总我们的模拟数据。注意底部图(汇总值)如何有效去除了系列中的间歇性。图片由作者提供。
在上图中,我们可以看到汇总对模拟数据的影响。这里,我们对五个时间步进行了汇总。结果汇总序列如底部图所示,不再是间歇性的,因为我们去除了所有零值。
一旦数据被汇总,简单的指数平滑再次用于预测汇总后的序列。
然后,我们将预测值拆解回原始频率。例如,如果每小时数据被聚合到每日数据,那么每个预测值将除以 24(因为一天有 24 小时)来得到拆解后的预测值。
如何选择聚合水平
当然,聚合水平对预测和模型性能有很大影响。
如果聚合过大,例如从每小时数据聚合到每周数据,那么可能会丢失很多信息。
如果聚合过小,则结果系列也可能是间歇性的,此时传统的预测方法将无法适用。
尽管没有明确的答案来选择聚合水平,但在statsforecast中实现的一种方法是计算非零值之间所有间隔的长度,并取间隔的平均值作为聚合水平。
例如,如果您的间歇性系列中非零值之间的间隔为[3, 5, 4],则聚合水平将为 4。
在最佳情况下,这种方法可以完全消除间歇性。否则,只有少数零值会残留,这不会对指数平滑产生很大影响。
ADIDA 的实际应用
现在,让我们在模拟数据上实现 ADIDA,并查看我们获得的预测结果。
使用statsforecast,实现仍然简单明了,我们只需更改模型,但管道保持不变。
from statsforecast.models import ADIDA
models = [CrostonOptimized(), ADIDA()]
sf = StatsForecast(
df=sim_df,
models=models,
freq='H',
n_jobs=-1
)
cv_df = sf.cross_validation(
df=sim_df,
h=1,
step_size=1,
n_windows=50
)
cv_df.index = np.arange(50, 100, 1)
然后,我们绘制预测图,查看其与 Croston 方法的比较表现如何。
使用 ADIDA(虚线)和 Croston 方法(虚线)进行预测。我们可以看到,ADIDA 对零值期间的反应更为明显,通过降低预测值来应对。图片由作者提供。
在上图中,我们可以看到 ADIDA 对零值期间的反应明显更强。尽管 Croston 的预测在观察到零值时保持不变,但 ADIDA 将逐渐降低预测曲线,因此更接近实际数据。
虽然 ADIDA 考虑了单一的聚合水平,但模型中提出了一次迭代来考虑多个聚合水平。这是我们在下一节中研究的内容。
间歇性多重聚合预测算法(IMAPA)
如前所述,ADIDA 只考虑一个聚合水平。
然而,信息可能会从不同聚合水平的系列中恢复。
例如,给定每小时数据,如果我们将数据聚合到每日、每周或每月,不同的模式会出现。
这就是间歇性多重聚合预测算法或 IMAPA 的基本思想。
数据再次被聚合,但在多个水平上。然后,就像 ADIDA 一样,使用简单指数平滑来生成每个聚合水平上的预测值。之后,每个预测值被拆解,就像在 ADIDA 中一样。
最终预测是通过在每个聚合级别上取每个预测值的平均值得到的。
因此,我们可以将 IMAPA 看作是在不同聚合级别上多次运行 ADIDA,然后简单地平均预测值以获得最终预测。
记住这些,让我们看看 IMAPA 在我们模拟数据上的表现。
IMAPA 的实际应用
仍然使用statsforecast,我们只需将 IMAPA 算法添加到我们的管道中。
from statsforecast.models import IMAPA
models = [ADIDA(), IMAPA()]
sf = StatsForecast(
df=sim_df,
models=models,
freq='H',
n_jobs=-1
)
cv_df = sf.cross_validation(
df=sim_df,
h=1,
step_size=1,
n_windows=50
)
cv_df.index = np.arange(50, 100, 1)
然后,我们可以绘制预测图。
使用 IMAPA 进行预测。在这种情况下,它给出了与 ADIDA 相同的结果。图片来源于作者。
看上面的图,我们注意到两条曲线重叠,这意味着 IMAPA 和 ADIDA 在这种情况下给出了相同的预测结果。
尽管这有些令人失望,但请记住,我们正在使用模拟数据,很快我们将处理实际数据集。
在此之前,我们还有一种方法需要探索。
Teunter-Syntetos-Babai 模型(TSB)
Teunter-Syntetos-Babais 模型,或 TSB,提出了对 Croston 方法的改进。
正如我们之前所见,Croston 方法的预测在零值期间保持不变。这意味着在多个零值期间,预测可能会过时。
换句话说,Croston 方法忽略了过时的风险,这种风险发生在非零值被越来越长的零需求期隔开时。
这在低需求产品的库存管理中尤其重要,因为公司可能会持有多年未使用的库存,这会带来成本。因此,他们必须评估过时的风险,以确定是否可以处理过时库存。
这就是 TSB 模型的作用。它不会考虑需求间隔(即零值期间),而是考虑需求概率,定义为:
需求概率的定义。图片来源于 R. Teunter、A. Syntetos、M. Babai,来自间歇性需求:将预测与库存过时性联系起来
尽管这似乎是一个小差异,但实际上可能会产生很大的影响。使用 Croston 方法时,需求间隔只能在观察到非零值后进行更新。
另一方面,需求概率在每个时间步长上都会更新,使模型更具灵活性。
为了进行预测,模型还会从原始序列中创建两个序列:
-
一个包含仅非零值的序列(也称为需求)
-
另一个是需求概率
每个序列的预测是通过简单的指数平滑完成的。然后,通过将需求量与需求概率相乘,得到最终预测。
记住这些,让我们将 TSB 模型应用于我们的模拟数据。
TSB 的实际应用
与 Croston 方法的优化版本不同,statsforecast 中的 TSB 实现需要我们为每个序列指定平滑参数。
这意味着我们需要手动优化这些参数。现在,我们只使用 0.1 作为两个参数,只是为了看看模型在我们模拟数据上的表现。
from statsforecast.models import TSB
models = [TSB(0.1, 0.1), CrostonClassic()]
sf = StatsForecast(
df=sim_df,
models=models,
freq='H',
n_jobs=-1
)
cv_df = sf.cross_validation(
df=sim_df,
h=1,
step_size=1,
n_windows=50
)
cv_df.index = np.arange(50, 100, 1)
然后,我们可以绘制预测图。
使用 TSB 进行预测。我们可以看到,在零需求期间,使用需求概率更新预测曲线,与 Croston 方法不同。图片由作者提供。
看上面的图,我们可以看到使用需求概率的效果很大,因为预测在无需求期间下降,而不是保持不变。
现在我们已经涵盖了许多间歇性时间序列的预测模型,让我们在一个小的 capstone 项目中应用我们的知识。
Capstone 项目 — 预测风力发电机的功率输出
风力发电机是可再生能源的来源,但由于风的不可预测性,它们不幸地不可靠。
有时,功率输出可能非常大,而其他时候,它可能非常小。
也可能有风速过强的天,因此风力发电机停机,不产生电力。此外,风速不足也会导致没有电力产生。
因此,我们可以看到风力发电机的功率输出是一个间歇性时间序列。
提醒一下,你可以在GitHub上查看这个项目的完整源代码。
数据准备
我们首先读取数据并将其格式化,以便我们可以使用statsforecast。我们删除不必要的列,并将时间格式化为时间戳。最后,我们创建一个unique_id列,并适当地重命名列。
df = pd.read_csv('TexasTurbine.csv')
df = df.drop(['Wind speed | (m/s)', 'Wind direction | (deg)', 'Pressure | (atm)', "Air temperature | ('C)"], axis=1)
start_date = pd.to_datetime('2023-01-01 00:00:00')
end_date = pd.to_datetime('2023-12-31 23:00:00')
date_range = pd.date_range(start=start_date, end=end_date, freq='H')
df['ds'] = date_range
df = df.rename(columns={'System power generated | (kW)': "y"})
df = df.drop(['Time stamp'], axis=1)
df['unique_id'] = 1
df = df[['unique_id', 'ds', 'y']]
格式化数据集的前五行。图片由作者提供。
这样,我们的数据就按照statsforecast的期望格式化了。请记住,unique_id列用于识别同一数据集中不同的时间序列。在我们的案例中,我们只有一个序列,所以unique_id对所有行都是恒定的。
然后,我们可以可视化我们的数据。在这里,我们仅关注前 200 个时间步骤,因为我们有一个相当大的数据集。
fig, ax = plt.subplots( figsize=(10,8))
ax.bar(df.index, df['y'], color='grey', width=0.1)
ax.set_ylabel('Wind power (kW)')
ax.set_xlabel('Time steps')
plt.xlim(0, 200)
plt.tight_layout()
德克萨斯州风力发电机的功率输出。图片由作者提供。
从上图中,我们可以看到数据的间歇性。我们确实注意到零值期间,并且还注意到高功率输出和低功率输出之间的非常大波动。
现在,让我们尝试预测风力发电机的功率输出。我们将考虑三个不同的预测周期:
-
一小时
-
一天
-
一周
对于每个预测周期,我们将使用平均绝对误差(MAE)来评估每个模型的性能,并选择最佳模型。我们的基准模型将是简单指数平滑。
预测下一小时
要测试不同的模型,我们只需将它们列出在 Python 列表中。
在这里,我们立即使用了优化后的 Croston 方法,以获得平滑参数的最佳值。
from statsforecast.models import SimpleExponentialSmoothingOptimized as SESOpt
models = [CrostonOptimized(), ADIDA(), IMAPA(), TSB(0.2, 0.2), SESOpt()]
完成这些操作后,我们可以初始化 Statsforecast 对象以传入我们的数据集,并指定数据的频率。
sf = StatsForecast(
df=df,
models=models,
freq='H',
n_jobs=-1 # use all computing power available
)
然后,我们进行交叉验证,将预测值与已知值进行比较。由于我们正在预测下一个小时,我们将预测期设置为 1。此外,我们在 50 次预测中评估我们的模型。
h_cv_df = sf.cross_validation(
df=df,
h=1, # Horizon is 1, since we forecast the next hour
step_size=1, # Move the window by 1 time step
n_windows=50 # Make 50 windows of cross-validation
)
h_cv_df.index = np.arange(8709, 8759, 1)
这会生成一个包含每个模型的预测值和实际值的 DataFrame。这使我们能够将预测值与实际值进行绘图。
fig, ax = plt.subplots( figsize=(10,8))
ax.bar(h_cv_df.index, h_cv_df['y'], color='lightgrey')
ax.plot(h_cv_df.index, h_cv_df['CrostonOptimized'], ls='--', label='Croston')
ax.plot(h_cv_df.index, h_cv_df['ADIDA'], ls=':', label='ADIDA')
ax.plot(h_cv_df.index, h_cv_df['TSB'], ls='-.', label='TSB')
ax.plot(h_cv_df.index, h_cv_df['SESOpt'], ls=':', label='SES')
ax.set_ylabel('Wind power (kW)')
ax.set_xlabel('Time steps')
ax.legend(loc='best')
plt.tight_layout()
预测风力涡轮机下一小时的功率输出。图片由作者提供。
从上图中,我们注意到两件事。
首先,我没有绘制 IMAPA 的曲线,因为它与 ADIDA 给出的预测完全相同。
其次,简单指数平滑在预测下一个时间步时似乎做得非常好,因为它的曲线比其他模型更接近实际值。
为了验证这一点,让我们计算每个模型的 MAE,并创建一个条形图以识别最佳模型。
from sklearn.metrics import mean_absolute_error
mae_croston_h = mean_absolute_error(h_cv_df['y'], h_cv_df['CrostonOptimized'])
mae_adida_h = mean_absolute_error(h_cv_df['y'], h_cv_df['ADIDA'])
mae_tsb_h = mean_absolute_error(h_cv_df['y'], h_cv_df['TSB'])
mae_ses_h = mean_absolute_error(h_cv_df['y'], h_cv_df['SESOpt'])
y = [mae_croston_h, mae_adida_h, mae_tsb_h, mae_ses_h]
x = ['Croston', 'ADIDA', 'TSB', 'SES']
fig, ax = plt.subplots()
ax.bar(x, y, width=0.4)
ax.set_xlabel('Models')
ax.set_ylabel('MAE (kW)')
ax.set_xlabel('Models')
ax.set_ylim(0, 500)
for index, value in enumerate(y):
plt.text(x=index, y=value + 5, s=str(round(value,2)), ha='center')
plt.tight_layout()
每个模型在预测下一个小时时的 MAE。这里,SES 是最佳模型。图片由作者提供。
毫不意外,简单指数平滑是最佳模型,因为它获得了最低的 MAE。在这种情况下,我们的基线似乎在预测下一个时间步时表现最佳。
让我们看看模型在预测下一天时的表现。
预测下一天
为了预测下一天,Statforecast 对象保持不变。
现在,我们只需将预测期设置为 24 小时,并将交叉验证窗口移动 24 个时间步。这里,我们进行了五轮交叉验证。
d_cv_df = sf.cross_validation(
df=df,
h=24,
step_size=24,
n_windows=5
)
d_cv_df.index = np.arange(8639, 8759, 1)
然后,我们可以绘制预测值和实际值。
预测接下来的 24 小时的功率输出。图片由作者提供。
在上图中,我们注意到我们的预测在整个预测期内保持平稳,这很正常,因为每个模型输出的是一个常数值。
我们还可以看到,当预测时间跨度较长时,简单指数平滑的表现迅速下降。显然,简单指数平滑对零值周期处理不好。
使用 MAE 评估每个模型会得到以下结果。
每个模型在预测下一天时的 MAE。这里,Croston 是最佳模型。图片由作者提供。
从上图中,我们可以看到优化后的 Croston 方法是表现最佳的模型,获得了最低的 MAE。我们还发现,简单指数平滑在较长预测期的表现不如 Croston 和 TSB。
另外,请记住,由于我们增加了预测范围,误差也增加,这是可以预期的。我们预测的时间越长,离实际值就越远。
最后,让我们将预测范围设置为一周。
预测下周
为了预测下周,我们将预测范围设置为 168 个时间步长,因为一周有 168 小时,我们的数据是按小时记录的。
w_cv_df = sf.cross_validation(
df=df,
h=168,
step_size=168,
n_windows=2
)
w_cv_df.index = np.arange(8423, 8759, 1)
再次,我们可以绘制每个模型的预测图。
预测下周的电力输出。图像由作者提供。
上图稍显平淡,因为我们可以清楚地看到我们的模型与实际值相距甚远。这是可以预期的,因为我们的预测范围相当长。
评估我们的模型得到了以下结果。
预测下周每个模型的 MAE。再次强调,Croston 是最好的模型。图像由作者提供。
从上图来看,我们再次看到 Croston 的方法是最好的模型,因为它达到了最低的 MAE。
有趣的是,我们还注意到,与预测次日相比,尽管预测期是原来的七倍,MAE 并没有显著增加。
不过,在实践中,我怀疑预测一个常数值一周真的能帮助做决策或计划未来。
结论
我们已经看到,间歇性时间序列提出了有趣的预测挑战,因为传统模型处理零值期间的能力较差。
我们探索了不同的预测模型,如 Croston 方法、ADIDA、IMAPA 和 TSB,每种方法都提供了预测稀疏时间序列的新方法。
恭喜你看到最后,非常感谢你的阅读!希望你喜欢,并且学到了新的东西!
想掌握时间序列预测吗?那就查看Python 中的应用时间序列预测。这是唯一一个使用 Python 在 15 个引导的实际项目中实现统计、深度学习和最先进模型的课程。
干杯 🍻
支持我
喜欢我的工作吗?通过买杯咖啡来支持我,这是一种简单的方式来鼓励我,同时我也能享用一杯咖啡!如果你愿意,只需点击下面的按钮 👇
使用 Granger 因果关系进行预测:检查时间序列的虚假相关
使用机器学习方法破解 Granger 因果关系测试
·发表于 Towards Data Science ·6 分钟阅读·2023 年 4 月 6 日
--
Phoenix Han的照片,来源于 Unsplash
在时间序列预测中,通常通过图形化数据进行检查是非常有帮助的。这有助于我们理解我们所分析现象的动态,并据此做出决策。尽管拥有一个色彩斑斓的时间序列图可能非常迷人,但它可能会导致不正确的结论。时间序列很棘手,因为无关事件往往会在视觉上看起来有关联。
虚假相关的一个示例 [来源]
作为理性个体,我们可以轻易否定因被床单缠绕而死亡的人数与人均奶酪消费量之间的任何关系。我们可以断言存在虚假的(虚假)相关,因为没有任何东西可以将这两个事件联系起来,即使我们在这两个领域都不是专家。
从事数据工作的人知道这些模式可能经常发生,尤其是在我们难以解释上下文和区分真实与虚假相关时。因此,开发能够帮助识别这些情况的方法是至关重要的。
用于检测虚假相关的最著名技术之一是 Granger 因果关系测试。
Granger 因果关系建立在这样一种直觉上:如果信号 Y1 “Granger 引起” 另一个信号 Y2,那么 Y1 的滞后(即过去的观察)应包含有助于预测 Y2 的信息,以及 Y2 过去观察中包含的信息。
可能的 Granger 因果关系示例 [图片由作者提供]
测试 Granger 因果关系并不意味着 Y1 必须是 Y2 的原因。这仅仅意味着 Y1 的过去值足够好,可以改善对 Y2 未来值的预测。由此,我们可能得出一个朴素的因果关系定义。
采用 Granger 因果关系检验意味着对基础数据有严格的假设(即平稳性和线性依赖),这些假设在现实世界应用中可能难以满足。因此,在本文中,我们提出了一种 Granger 因果关系检验的泛化方法,采用了简单的机器学习方法,并涉及预测算法的使用。
实验设置
本文的范围内,我们模拟了两个不同的时间序列,这些序列是自回归过程的结果。
模拟的 AR 过程 [图片由作者提供]
两个序列与其过去的某些时间步长存在相关性(自相关)。
AR 过程的自相关 [图片由作者提供]
时间序列表现出整体皮尔逊相关系数为 0.637,且在时间上保持离散的正相关关系。
AR 过程的皮尔逊相关性 [图片由作者提供]
初看之下,这似乎表明我们面对的是两个具有正相关的事件。我们用皮尔逊相关系数来表示两个变量之间的相关性。这是测量变量之间线性关系的最常用统计量。如此常见,以至于人们常常错误地将其解释为因果关系。这可能是一个错误!皮尔逊相关仅涉及两个变量的均值和标准差。我们不能据此得出关于它们依赖关系的任何结论。
皮尔逊相关公式 [图片由作者提供]
在我们模拟的场景中,正相关仅仅是数学结果,因为我们知道这两个序列仅在一个方向上有关联。更确切地说,Y1 的过去值与 Y2 的实际值线性相关(反之则不成立)。我们的目标是对此声明进行实际演示。
Granger 预测
以经典方式进行 Granger 因果关系检验,意味着验证时间序列(Y1)的过去值是否对另一时间序列(Y2)的当前值有统计学上显著的影响。这是通过在滞后序列值上运行线性模型来完成的。
检验的零假设是 Y1 的过去值对应的系数为零。如果 p 值低于特定阈值,我们将拒绝零假设。在这种情况下,Y1 不会 Granger 引起 Y2。
如果我们对预测模型的残差进行相同的检查会怎么样?
换句话说,我们验证了添加 Y1 的过去值是否能改善仅使用目标(Y2)滞后观察值的模型的性能。
作为第一步,我们对 Y1 和 Y2 进行两个自回归模型的拟合,不添加额外的外生变量,并存储在测试数据上获得的预测结果。
forecaster = ForecastingCascade(
RandomForestRegressor(30, random_state=42, n_jobs=-1),
lags=lags,
use_exog=False,
)
model_y1 = clone(forecaster).fit(None, df_train['y1'])
model_y2 = clone(forecaster).fit(None, df_train['y2'])
y1_pred = np.concatenate([
model_y1.predict(
[[0.]],
last_y=df['y1'].iloc[:i]
) for i in range(len(df_train), len(df_train) + len(df_test))
])
y2_pred = np.concatenate([
model_y2.predict(
[[0.]],
last_y=df['y2'].iloc[:i]
) for i in range(len(df_train), len(df_train) + len(df_test))
])
其次,我们重复相同的预测过程,但添加了滞后的外生变量(即在预测 Y1 时使用 Y2 的过去值加上 Y1 的过去值)。
forecaster = ForecastingCascade(
make_pipeline(
FunctionTransformer(
lambda x: x[:,1:] # remove current values of exog series
),
RandomForestRegressor(30, random_state=42, n_jobs=-1)
),
lags=lags,
use_exog=True,
exog_lags=lags,
)
model_y1y2 = clone(forecaster).fit(df_train[['y2']], df_train['y1'])
model_y2y1 = clone(forecaster).fit(df_train[['y1']], df_train['y2'])
y1y2_pred = np.concatenate([
model_y1y2.predict(
pd.DataFrame({'y2': [0.]}),
last_y=df['y1'].iloc[:i],
last_X=df[['y2']].iloc[:i]
) for i in range(len(df_train), len(df_train) + len(df_test))
])
y2y1_pred = np.concatenate([
model_y2y1.predict(
pd.DataFrame({'y1': [0.]}),
last_y=df['y2'].iloc[:i],
last_X=df[['y1']].iloc[:i]
) for i in range(len(df_train), len(df_train) + len(df_test))
])
在预测阶段结束时,我们存储了 4 种不同模型的预测结果(两个用于预测 Y1,另外两个用于预测 Y2)。现在是结果比较的时间。
对所有预测类型的样本级别计算平方残差。对相同预测目标的平方残差分布进行综合分析。我们使用标准的 Kolmogorov-Smirnov 检验来检查分布差异。
平方残差分布的比较 [图片由作者提供]
对于 Y1 的预测在添加和不添加 Y2 特征的情况下似乎是一样的。
平方残差分布的比较 [图片由作者提供]
相反,Y2 的预测在添加和不添加 Y1 特征的情况下有显著差异。这意味着 Y1 对预测 Y2 有正面影响,即 Y1 格兰杰导致 Y2(反之则不成立)。
总结
在这篇文章中,我们提出了一种替代标准格兰杰因果检验的方法,以验证时间序列领域中的因果动态。我们没有仅仅依赖于皮尔逊相关系数来得出数据结论。我们以经验的方式分析了可用事件的相互影响的可能性,识别出虚假的关系。所提出的方法易于使用且具有适应性,假设较少,适合用于任何时间序列分析过程。
保持联系: Linkedin
忘掉 SQLite,改用 DuckDB——然后感谢我
原文:
towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777
DuckDB 及其 Python 集成介绍
·发表于 Towards Data Science ·8 分钟阅读·2023 年 3 月 16 日
--
图片来源 Krzysztof Niewolny 于 Unsplash
我们程序员往往在需要在本地环境中使用嵌入式数据库时默认选择 SQLite。虽然大多数时候这工作得很好,但就像骑自行车旅行 100 公里:可能不是最佳选择。
介绍 DuckDB。
我第一次了解到 DuckDB 是在 2022 年 9 月,在格拉纳达的 PyCon 西班牙上。现在,经过 6 个月的使用,我已经离不开它了。我希望通过为我的程序员和数据相关专业人士提供对这个出色分析数据库系统的介绍来回馈社区。
在这篇文章中,我将讨论以下主要点:
-
DuckDB 简介:它是什么,为什么你应该使用它以及何时使用。
-
DuckDB 集成到 Python 中。
准备好!
如果你无法看到整个故事,考虑使用我的推荐链接以获得对所有 Medium 故事的无限访问:
medium.com/@polmarin/membership
什么是 DuckDB?
如果你查看 DuckDB 的官网[1],你会在首页看到的第一件事是:DuckDB 是一个内嵌 SQL OLAP 数据库管理系统。
让我们尝试解读这句话,因为其中包含相关信息。
-
内嵌 SQL 意味着 DuckDB 的功能在你的应用程序中运行,而不是在你的应用程序连接的外部进程中。换句话说:没有客户端发送指令,也没有服务器来读取和处理它们。SQLite 的工作方式相同,而 PostgreSQL、MySQL 等则不然。
-
OLAP 代表在线分析处理,微软将其定义为一种组织大型业务数据库并支持复杂分析的技术。它可以用于执行复杂的分析查询而不会对事务系统产生负面影响[2]。另一个 OLAP 数据库管理系统的例子是 Teradata。
所以基本上,如果你在寻找一个无服务器的数据分析数据库管理系统,DuckDB 是一个很好的选择。我强烈建议你查看 Dr. Mark Raasveldt 和 Dr. Hannes Mühleisen [3]— 两位最重要的 DuckDB 开发者 — 的出色同行评审论文,以了解 DuckDB 试图填补的空白。
此外,它是一个支持 SQL 的关系数据库管理系统(DBMS)。这就是为什么我们将它与其他具有相同特征的 DBMS(如 SQLite 或 PostgreSQL)进行比较的原因。
为什么选择 DuckDB?
我们现在知道 DuckDB 在数据库行业中的作用。但为什么我们应该选择它,而不是其他许多可能的选项呢?
在数据库管理系统中,没有一刀切的解决方案,DuckDB 也不例外。我们将讨论它的一些特性,以帮助你决定何时使用它或不使用它。
总而言之,它是一个高性能的工具。如他们的 GitHub 页面[4]所示:“它被设计为快速、可靠且易于使用。” 进一步了解…
-
它是为了支持分析查询工作负载(OLAP)而创建的。他们的做法是通过向量化查询执行(面向列),而其他之前提到的 DBMS(SQLite、PostgreSQL…)则按顺序处理每一行。这就是为什么其性能会提高。
-
DuckDB 采用了 SQLite 最好的特性:简洁性。简洁的安装和嵌入式进程操作是 DuckDB 开发者在看到 SQLite 的成功后,为这个 DBMS 选择的特点。
此外,DuckDB 没有外部依赖项,也不需要安装、更新或维护服务器软件。如前所述,它完全嵌入,这具有数据在数据库之间高效传输的额外优势。
-
技术娴熟的创建者。他们是一个研究小组,创建它是为了开发一个稳定且成熟的数据库系统。这是通过密集而彻底的测试来实现的,测试套件目前包含了数百万个查询,改编自 SQLite、PostgreSQL 和 MonetDB 的测试套件。
-
它是完整的。它支持 SQL 中的复杂查询,提供事务保证(你肯定听说过的 ACID 特性),支持二级索引以加快查询速度……而且,更重要的是,它与 Python 和 R 深度集成,以便进行高效的交互式数据分析。
它还提供了 C、C++、Java 等语言的 API。
-
免费且开源。再好不过了。
这些是官方优势。
但还有更多,我想强调一点:DuckDB 不一定要替代 Pandas。它们可以协同工作,如果你像我一样是 Pandas 的粉丝,你可以在 Pandas 中使用 DuckDB 进行高效的 SQL 操作。
这真是太棒了。
你可以在 DuckDB 的网站[1]上找到更完整的解释。
什么时候使用 DuckDB?
这确实取决于你的个人偏好,但让我们回到其共同创始人发布的论文[3](我强烈推荐你阅读,它只有 4 页,内容非常宝贵)。
他们解释说,嵌入式分析数据管理有着明显的需求。SQLite 是嵌入式的,但如果我们想用它进行全面的数据分析,它太慢了。他们继续说:“这种需求来自两个主要来源:交互式数据分析和‘边缘’计算。”
所以这是 DuckDB 的前两大用例:
-
交互式数据分析。大多数数据专业人士现在在本地环境中使用 R 或 Python 库,如dplyr或Pandas来处理他们从数据库中检索的数据。DuckDB 提供了在本地开发中利用 SQL 效率的可能性,而不会影响性能。你可以在不放弃你喜欢的编程语言的情况下享受这些好处(稍后会详细介绍)。
-
边缘计算。根据维基百科的定义,“边缘计算是一种分布式计算范式,将计算和数据存储更接近数据源。” [5] 使用嵌入式 DBMS,它可以更接近这一点!
DuckDB 可以在不同的环境中安装和使用:Python、R、Java、node.js、Julia、C++……在这里,我们将重点关注 Python,你很快会看到它的使用是多么简单。
使用 DuckDB 与 Python(入门)
打开你的终端并导航到所需的目录,因为我们马上就要开始了。创建一个新的虚拟环境——或者不创建——并安装 DuckDB:
pip install duckdb==0.7.1
如果你需要另一个版本,请删除或更新。
现在进入有趣的部分。为了使事情更有趣,我将使用我在 Kaggle 上找到的关于 Spotify 最流行歌曲的真实数据[6]。我将使用典型的 Jupyter Notebook 进行操作。
许可证: CC0: 公共领域
由于我们获得的数据是两个 CSV 文件——Features.csv 和 Streams.csv——,我们需要创建一个新数据库并将其加载:
import duckdb
# Create DB (embedded DBMS)
conn = duckdb.connect('spotiStats.duckdb')
c = conn.cursor()
# Create tables by importing the content from the CSVs
c.execute(
"CREATE TABLE features AS SELECT * FROM read_csv_auto('Features.csv');"
)
c.execute(
"CREATE TABLE streams AS SELECT * FROM read_csv_auto('Streams.csv');"
)
就这样,我们创建了一个全新的数据库,添加了两个新表,并用所有数据填充它们。仅用 4 行简单的代码(如果考虑到导入,则为 5 行)。很酷吧?
让我们展示一下流媒体表中的内容:
c.sql("SELECT * FROM streams")
所有时间流媒体前 100 首歌曲的可视化— 作者截图。
让我们开始进行一些分析任务。例如,我想知道前 100 首歌曲中有多少首是在 2000 年之前的。这是其中一种方法:
c.sql('''
SELECT *
FROM streams
WHERE regexp_extract("Release Date", '\d{2}$') > '23'
''')
所有时间流媒体前 100 首歌曲中的 2000 年之前的歌曲 — 作者截图。
我之前提到过,使用 DuckDB 和 Pandas 同时工作是多么简单。这里有一种使用 Pandas 做到相同的方式:
df = c.sql('SELECT * FROM streams').df()
df[df['Release Date'].apply(lambda x: x[-2:] > '23')]
我所做的只是将初始查询转换成 DataFrame,然后以 Pandas 的方式应用过滤器。结果是一样的,但它们的性能如何呢?
>>> %timeit df[df['Release Date'].apply(lambda x: x[-2:] > '23')]
434 µs ± 25.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit c.sql('SELECT * FROM streams WHERE regexp_extract("Release Date", \'\d{2}$\') > \'23\'')
112 µs ± 25.3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
你看到了吗?操作相当简单:我们只对一个 100 行的表应用了一个简单的过滤器。但如果我们将其与 DuckDB 实现进行比较,使用 Pandas 的执行时间几乎是其 4 倍。
想象一下,如果我们尝试一个更为详尽的分析操作……改进可能会非常巨大。
我认为再提供更多的示例意义不大,因为这将使 DuckDB 的介绍变成 SQL 入门。这并不是我想要的。
但请随意使用你可能拥有的任何数据集,并开始在 DuckDB 数据库上使用 SQL。你会很快看到它的好处。
为了结束这个简短的介绍,让我们将最后的结果(2000 年前的歌曲)导出为 parquet 文件——因为它们始终是比传统 CSV 更好的替代品。同样,这将是非常简单的:
c.execute('''
COPY (
SELECT
*
FROM
streams
WHERE
regexp_extract("Release Date", '\d{2}$') > '23'
)
TO 'old_songs.parquet' (FORMAT PARQUET);
''')
我所做的只是将之前的查询放在括号内,DuckDB 只会将查询结果复制到 old_songs.parquet
文件中。
瞧瞧。
结论
DuckDB 对我来说是一次生活的改变,我认为它也可以对很多人产生影响。
希望这篇文章既有趣又有信息量。它并不打算成为教程或指南,所以我没有详细讲解代码示例,但我相信已经有足够的信息来理解主要观点。
**Thanks for reading the post!**
I really hope you enjoyed it and found it insightful.
Follow me for more content like this one, it helps a lot!
**@polmarin**
如果你想进一步支持我,请考虑通过下面的链接订阅 Medium 的会员:这不会花费你额外的钱,但将帮助我完成这个过程。非常感谢!
[## 使用我的推荐链接加入 Medium — Pol Marin
阅读 Pol Marin 的每一篇故事(以及 Medium 上其他成千上万的作者的故事)。你的会员费用直接支持 Pol……
medium.com](https://medium.com/@polmarin/membership?source=post_page-----df76ee9bb777--------------------------------)
资源
[1] DuckDB
[2] 在线分析处理 (OLAP) — Azure 架构中心
[3] Mark Raasveldt 和 Hannes Mühleisen. 2019. DuckDB: an Embeddable Analytical Database. In 2019 International Conference on Management of Data (SIGMOD ’19), June 30-July 5, 2019, Amsterdam, Netherlands. ACM, New York, NY, USA, 4 pages. doi.org/10.1145/3299869.3320212
[4] DuckDB — GitHub
[5] 边缘计算 — 维基百科
ETL 测试 — 如何正确测试你的数据管道
忘掉 2023 年的新数据趋势吧!这个根本的数据工程挑战仍然没有解决。
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 1 月 6 日
--
2023 年!ELT、反向 ETL、EtLT、数据网格、数据合同、FinOps 和现代数据栈等新数据范式(或流行词汇)已经进入主流数据对话。我们的数据团队仍在弄清楚哪些是炒作,哪些不是。
明天可能会有 10 种新范式,但一些数据工程中的根本挑战——如数据质量——仍然相关且尚未完全解决(我认为我们永远无法完全解决这个问题)。提高数据质量的第一步是对我们的数据管道进行严格测试。
在本文中,让我们探讨在有效测试数据管道时面临的挑战以及如何为您的组织建立一个全面的测试策略。
为什么实现数据质量很难?
在软件应用开发领域,提高软件质量意味着进行严格的测试。同样,在数据工程中,我们需要一个全面的测试策略,以确保生产中的数据质量。
大多数数据团队都面临紧迫的截止日期。因此,数据工程的文化使得我们最终构建的管道是在一周结束前提供数据,而不是融入长期有价值的最佳实践。
-
在 ETL 测试中,我们通常需要比较来自不同源系统的大量数据(例如数百万条记录)。我们在比较经过复杂 SQL 查询或 Spark 作业处理后的转换数据。
-
不是所有的数据工程师(及数据工程领导者)都有软件工程背景,也不都擅长软件工程开发原则和最佳实践。
-
自动化测试套件和数据产品的自动化部署/发布仍未成为主流。
来源:由作者创建
ETL 测试是一个以数据为中心的测试过程。为了有效地测试我们的管道,我们需要类似生产的数据(在体积、多样性和速度方面)。
来源:由作者创建
获得类似生产的数据很困难。以下是不同公司中的数据团队如何解决获取合适数据以测试数据管道的问题。
1. 模拟数据:
优点: 这种方法被我们所有数据工程师广泛使用,因为模拟数据的创建容易,而且有合成数据生成工具(如 Faker)可用。
缺点: 模拟数据在体积、多样性或速度方面无法反映生产数据。
2. 从生产数据中抽样到测试/开发环境:
优点: 复制部分生产数据比复制大量生产数据要容易。
缺点: 应使用正确的抽样策略来确保样本反映真实世界的生产数据。成功在样本生产数据上运行的测试可能在实际生产数据上失败,因为数据的体积和多样性无法保证。
3. 复制所有生产数据到测试环境:
优点: 提供真实世界生产数据用于测试。
缺点: 如果生产数据包含个人身份信息(PII),可能会导致数据隐私违规。如果生产数据不断变化,则测试/开发环境中的生产数据副本将变得陈旧,需要不断更新。体积和多样性有保证,但速度没有保证。
4. 复制匿名化的生产数据到测试环境:
优点: 提供真实世界生产数据用于测试。符合所有数据隐私法规。
缺点:再次强调,生产数据的持续变化意味着测试环境中的数据变得过时,并需要频繁刷新。每次从生产环境中复制数据时都需要进行 PII 匿名化。每次手动执行匿名化步骤并维护长期运行的测试数据环境容易出错且资源消耗大。
5. 使用数据版本控制工具将生产数据镜像到开发/测试环境:
优点:可用的真实生产数据。通过类似 Git 的 API 提供的自动化短期测试环境。
缺点:将一个新工具(如lakeFS)添加到现有的数据堆栈中。
注意:如果你对 ETL 测试中的数据版本控制工具感兴趣,这里有一个快速指南,供你参考。
关于如何使用 lakeFS 创建不同数据环境以进行 ETL 测试的视频教程。来源:作者创建。
好了,假设你拥有正确的数据。接下来呢?
-
需要运行一系列全面的测试,以确保数据湖或数据仓库中数据的质量和可靠性。
-
你可以使用数据质量测试工具(如 Great Expectations、soda.io 等)进行这些测试,也可以构建一个内部测试套件。无论如何!
-
根据项目、数据集和业务的不同,需要执行不同类型的测试。然而,这里有一个由Irene整理的不同数据质量维度的基本测试检查表。
来源:www.scnsoft.com/blog/guide-to-data-quality-management
1. 一致性:
在上述备忘单的基础上,一致性检查应包括比较源系统和目标系统。在某些行业中,不一致的数据是允许的(是的!),并且误差的变化应低于指定阈值。因为不同的数据系统之间可能存在不一致,如果它们是异步更新的(得益于微服务和消息队列)。
例如,当你比较源和目标中最后一小时的客户订单数量时,结果可能会有所不同。但当你运行汇总测试(即一个月的订单数量)时,数据将会趋于一致。
2. 准确性:
你的检查应包括数据验证和领域值检查。例如,出生日期列不能有超过 200 年的值。
根据你的领域,数据应在特定范围内。一些业务 KPI 不能有特定值。例如,一个名为点击率的列不能有大于 1.0 的值,等等。
3. 完整性:
应包括记录数、列数、每列缺失值的百分比、空值的百分比、范围检查(min_value、max_value)以及基本统计数据,如均值、中位数、方差和数据分布(特定列中值的频率)等检查。
再次,根据业务领域的不同,你可以设定一个错误的阈值。在制药和金融领域,由于合规要求,数据团队力求 100%的数据验证和 0%的错误差异。
4. 数据可审计性:
比较源系统和数据系统的审计日志,确保每个转换或数据移动步骤都被捕捉并匹配。
数据血统工具支持数据审计。
5. 有序性:
这涉及到对数据和列格式检查的测试。列的数据类型、缺失列的百分比、列名不匹配的百分比、数据格式不匹配的百分比(一个来源中的日期可能是MMDDYYYY格式,而另一个来源中的日期可能是DDMMYYYY格式)。
6. 唯一性:
如名称所示,检查哪些列应具有唯一值、重复条目的百分比等。
7. 及时性:
延迟到达的数据是我们面临的一个常见挑战。这种情况发生在源系统生成的数据点在延迟后到达目标区域。例如,昨天的数据可能会进入今天的日期分区,并与今天的数据点一起处理。这会导致意外的数据错误。
因此,验证数据中的created_timestamp列与date_partition的一致性也非常关键。
1. 白盒测试:
上述七种测试类型属于白盒测试。另一种类型是对复杂数据转换进行单元测试。这是为了验证 KPI 定义和其他转换是否符合预期。
2. 黑盒测试:
数据管道包含许多动态部分——从多个数据源、复杂的数据转换到下游的并发数据消费者。因此,仅测试转换是不够的。需要对 ETL 管道进行端到端测试,以确保管道按预期工作。
3. 回归测试:
假设管道的某一部分发生了变化,必须确保其他部分由于这次变化没有出现回归错误。
这就是自动化 ETL 测试重要的地方。也就是说,在每次更改后,需要运行一套测试(有时也称为持续集成测试)。只有当测试套件成功运行时,才能将更改推送到生产环境中。
你可以使用像lakeFS 这样的工具来运行自动化测试,并实现数据湖的 CI/CD。
数据管道将处理后的数据从数据湖传送到下游的消费者,如业务仪表盘和机器…
4. 性能测试:
除了确保数据质量外,对 ETL 管道进行负载测试也是提高数据产品发布可靠性所必需的。
因此,分析 ETL 任务的运行时间和任务的执行顺序,以识别瓶颈。通常,当数据量增加时,会导致管道变慢。通过监控运行时间、优化分布式计算作业和调整硬件需求,可以解决这个问题。
这些是需要运行的全面测试清单。然而,我们中的大多数人在工作中只运行其中的一些。如果你是数据工程师,你们团队的 ETL 测试策略是怎样的?
感谢阅读!
如果你喜欢我的工作并想支持我…
忘掉 RAG,未来是 RAG-Fusion
原文:
towardsdatascience.com/forget-rag-the-future-is-rag-fusion-1147298d8ad1
搜索的下一前沿:检索增强生成遇上互惠排名融合和生成查询
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 10 月 6 日
--
RAG 融合的奇妙世界。图示由作者提供。
探索搜索技术近十年后,我可以诚实地说,没有什么比检索增强生成(RAG)的崛起更具颠覆性。这一系统通过使用生成 AI 的向量搜索来基于可信的数据生成直接答案,正在彻底改变搜索和信息检索。
在我的搜索项目中,实验使用 RAG 让我考虑了其潜在的增强功能;我相信 RAG 仍然过于有限,无法满足用户的需求,需要升级。
我的个人搜索系统(Project Ramble),其中我在 2022 年将我的 Obsidian 笔记与 GPT-3 结合的向量搜索连接起来。图像由作者提供。
别误解我的意思,RAG 非常出色,绝对是信息检索技术向正确方向迈进的一步。我自 2021 年 GPT-2 出现以来一直在使用 RAG,它在寻找有价值的信息时显著提高了我的生产力。RAG 有许多优点:
-
向量搜索融合: RAG 通过将向量搜索功能与生成模型结合,介绍了一种新颖的范式。这种融合使得从大型语言模型(LLM)生成更丰富、更具上下文感知的输出成为可能。
-
减少幻觉: RAG 显著减少了 LLM 的幻觉倾向,使生成的文本更加基于数据。
-
个人和职业效用: 从个人应用如筛选笔记到更专业的集成,RAG 展示了提升生产力和内容质量的多功能性,同时基于可信的数据源。
然而,我发现 RAG 有越来越多的局限性:
-
当前搜索技术的限制: RAG 受到与我们检索基础的词汇和向量搜索技术相同的限制。
-
人工搜索的低效: 人们在将他们想要的内容输入搜索系统时表现不佳,如拼写错误、模糊查询或词汇有限,这往往导致错过那些隐藏在明显顶级搜索结果之外的大量信息。虽然 RAG 提供了帮助,但并未完全解决这个问题。
-
搜索的过度简化: 我们现有的搜索范式将查询线性映射到答案,缺乏理解人类查询多维特性的深度。这种线性模型通常无法捕捉更复杂用户询问的细微差别和背景,导致结果相关性较低。
2023 年 RAG(检索增强生成)的搜索激增。截图由作者提供,来源于 2023 年 9 月 Google Trends。
那么,我们能做些什么来解决这些问题呢?我们需要一个不仅能检索我们所问内容,还能理解我们查询背后细微差别的系统,而不必依赖越来越高级的 LLM。认识到这些挑战,并受到可能性的启发,我开发了一个更精细的解决方案:RAG-Fusion。
为什么选择 RAG-Fusion?
-
填补空白: 通过生成多个用户查询并重新排名结果,解决 RAG 固有的局限性。
-
增强搜索: 利用互惠排序融合和自定义向量分数加权,以获取全面、准确的结果。
RAG-Fusion 致力于弥合用户明确询问的内容与他们实际意图之间的差距,逐步揭示那些通常隐藏的变革性知识。
多年前开始这段旅程时,我遗憾未能分享那些初期实验。但现在是时候弥补了。让我们深入探讨 RAG-Fusion 的技术细节。
深入探讨 RAG-Fusion 的机制
工具和技术栈
供那些只想查看代码并直接玩 RAG-Fusion 的人,请查看这里的 GitHub 仓库。
RAG Fusion 的基础三要素与 RAG 相似,涉及相同的三项关键技术:
-
一种通用编程语言,通常是 Python。
-
一个专用的向量搜索数据库,如 Elasticsearch 或 Pinecone,指导文档检索。
-
一个强大的大型语言模型,如 ChatGPT,用于生成文本。
RAG-Fusion 工作机制的示意图。图像由作者提供。
然而,与 RAG 不同,RAG-Fusion 通过几个额外步骤——查询生成和结果的重新排序,来区分自己。
RAG-Fusion 的工作流程:
-
查询复制的新方法: 通过 LLM 将用户的查询转换为类似但又不同的查询。
-
向量搜索的释放: 执行对原始查询及其新生成的查询兄弟的向量搜索。
-
智能重新排序: 使用互惠排名融合方法汇总和优化所有结果。
-
优雅的结局: 将精心挑选的结果与新的查询配对,引导大型语言模型生成考虑所有查询和重新排序结果列表的输出。
RAG-Fusion 代码 示例。图像来源于作者。
让我们更详细地了解这些步骤。
多查询生成
为什么使用多个查询?
在传统的搜索系统中,用户通常输入一个查询来寻找信息。虽然这种方法简单直接,但它也有其局限性。单一的查询可能无法捕捉用户感兴趣的全部范围,或者可能过于狭窄,导致结果不全面。这时,从不同角度生成多个查询就显得尤为重要。
技术实施(提示工程)
多查询生成的流程图:利用提示工程和自然语言模型来扩展搜索视野并提升结果质量。图像来源于作者。
使用提示工程来生成多个不仅与原始查询相似而且提供不同角度或视角的查询至关重要。
工作原理如下:
-
调用语言模型: 这个方法调用了一个语言模型(在这里是 chatGPT)。这种方法需要一组特定的指令,通常被称为“系统消息”,以指导模型。例如,这里的系统消息指示模型作为“AI 助手”进行操作。
-
自然语言查询: 模型根据原始查询生成多个查询。
-
多样性和覆盖范围: 这些查询不仅仅是随机变体。它们经过精心生成,以提供原始问题的不同视角。例如,如果原始查询是关于“气候变化的影响”,生成的查询可能包括“气候变化的经济后果”、“气候变化与公共健康”等角度。
这种方法确保了搜索过程考虑了更广泛的信息,从而提高了生成总结的质量和深度。
互惠排名融合(RRF)
为什么使用 RRF?
互惠排名融合(RRF)是一种将多个搜索结果列表的排名结合起来以生成单一统一排名的技术。与滑铁卢大学(加拿大)和 Google 合作开发,RRF 的作者表示,“比任何单独系统产生的结果更好,比标准的重新排序方法更好”。
RRF 算法,其中 k=60。图像来自 — 互惠排名融合优于 Condorcet 和单独的排名学习方法
通过结合来自不同查询的排名,我们增加了最相关文档出现在最终列表顶部的机会。RRF 特别有效,因为它不依赖于搜索引擎分配的绝对分数,而是依赖于相对排名,使其特别适合结合来自可能具有不同规模或分数分布的查询的结果。
通常,RRF 已被用于混合词汇和向量结果。尽管这种方法可以弥补向量搜索在查找特定术语(如首字母缩略词)时的特异性不足,但我对结果并不满意,因为结果往往更像是多个结果集的拼凑,因为相同的查询很少在词汇和向量搜索中得到相同的结果。
可以把 RRF 想象成那个在做决定前坚持听取每个人意见的人。只不过在这种情况下,它并不令人烦恼,而是有帮助的。更多的意见更好——在这种情况下,更准确。
技术实施
互惠排序融合位置重新排序系统。图片来源于作者。
函数 reciprocal_rank_fusion
接受一个搜索结果字典,每个键是一个查询,相应的值是按其与该查询的相关性排序的文档 ID 列表。然后,RRF 算法基于文档在不同列表中的排名计算一个新的分数,并对其进行排序,以创建最终的重新排名列表。
在计算融合分数后,该函数按这些分数的降序对文档进行排序,以获得最终的重新排名列表,然后返回。
生成输出
用户意图保留
使用多个查询的挑战之一是可能会稀释用户的原始意图。为了缓解这一点,我们指示模型在提示工程中给予原始查询更多权重。
技术实施
最后,重新排名的文档和所有查询被输入到一个 LLM 提示中,以典型的 RAG 方式生成输出,例如请求回应或摘要。
通过层叠这些技术和方法,RAG Fusion 提供了一种强大且细致的文本生成方法。它利用搜索技术和生成 AI 的最佳优势,产生高质量、可靠的输出。
RAG-Fusion 的优缺点
优势
1. 优越的源材料质量
使用 RAG Fusion 时,你的搜索深度不仅仅是‘增强’了——它被放大了。重新排名的相关文档列表意味着你不仅仅是在表面获取信息,而是深入到各种观点的海洋中。结构化的输出更易读,感觉直观上更值得信赖,这在一个对 AI 生成内容持怀疑态度的世界中至关重要。
2. 增强的用户意图对齐
从根本上说,RAG Fusion 设计为一种富有同理心的人工智能,它能够揭示用户努力表达但可能无法清晰表达的内容。利用多查询策略捕捉用户信息需求的多方面表现,从而提供全面的输出,与用户意图产生共鸣。
3. 结构化、深刻的输出
通过从多样化的来源汲取信息,模型可以提供组织良好且深刻的回答,预见后续问题并预先解决它们。
4. 自动更正用户查询
系统不仅能够解读,还能优化用户查询。通过生成多个查询变体,RAG Fusion 执行隐式的拼写和语法检查,从而提高搜索结果的准确性。
5. 处理复杂查询
人类语言在表达复杂或专业思想时常常会有所欠缺。系统作为语言催化剂,生成可能包含所需术语或专有名词的变体,以获得更具针对性和相关性的搜索结果。它还可以将更长、更复杂的查询拆解成更小、更易处理的部分,用于向量搜索。
6. 搜索中的意外发现
考虑“未知的未知”——你不知道自己需要的信息,直到你遇到它。RAG Fusion 允许这种意外发现。通过采用更广泛的查询范围,系统增加了发现信息的可能性,虽然这些信息并非明确寻求,但会成为用户的灵光一现。这使得 RAG Fusion 与其他传统搜索模型有所不同。
挑战
1. 过于冗长的风险
RAG-Fusion 的深度有时会导致信息泛滥。输出可能详细到让人不堪重负。可以把 RAG-Fusion 想象成那个喜欢过度解释的朋友——信息丰富,但有时你可能希望他们直接切入重点。
2. 平衡上下文窗口
包括多查询输入和多样化文档集可能会对语言模型的上下文窗口造成压力。想象一个舞台上挤满了演员,使得跟随剧情变得困难。对于上下文约束严格的模型,这可能导致输出不连贯甚至被截断。
道德与用户体验考量
大权在握带来大责任。使用 RAG Fusion 的强大能力来操控用户查询以改进结果,感觉像是跨越了一些道德灰色地带。平衡改善搜索结果与用户意图的完整性至关重要,我有一些你在实施此解决方案时应考虑的想法:
道德问题:
-
用户自主性: 操控用户查询有时可能偏离原意。必须考虑我们将多少控制权交给 AI,以及付出的代价。
-
透明度: 这不仅仅是关于更好的结果;用户应当知道他们的查询是否以及如何被调整。这种透明度对维持信任和尊重用户意图至关重要。
用户体验(UX)提升:
-
保留原始查询: RAG 融合优先考虑最初的用户查询,确保其在生成过程中的重要性。这是防止误解的一种保护措施。
-
过程可见性: 显示生成的查询和最终结果可以让用户透明地看到搜索的范围和深度。这有助于建立信任和理解。
UX/UI 实施技巧:
-
用户控制: 提供用户一个选项来切换 RAG 融合,使他们可以在手动控制和增强 AI 辅助之间进行选择。
-
指导与清晰度: 一个关于 RAG 融合工作原理的工具提示或简要说明可以帮助设定明确的用户期望。
如果要概括 RAG 融合的价值,那就是:它使我们更接近于 AI 本应做到的目标——增强人类潜力。
RAG 融合不仅仅是一个进步;它是对所有创新者的号召。它促使我们超越传统框架,重新构想“搜索”的全景。
对于搜索领域的从业者,我提出一个挑战:让我们不仅仅创建搜索系统,而是设计查询的解读者。
希望 RAG-Fusion 能激励你与我一起接受这个挑战。
深入了解GitHub 仓库,亲自动手实践代码,加入这场革命。
你必须从客户体验开始,然后逆向思考技术。—— 史蒂夫·乔布斯
参考文献
- 如果你想深入了解这个话题,我强烈推荐阅读 Trey Grainger、Doug Turnbull 和 Max Irwin 合著的《AI-Powered Search》。
计算机视觉中的前向映射和后向映射
原文:
towardsdatascience.com/forward-and-backward-mapping-for-computer-vision-833436e2472
前向映射和后向映射在图像变换中的应用
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 25 日
--
照片由Vadim Bogulov拍摄,来源于Unsplash
如果你想在没有 Premium Medium 账户的情况下阅读本文,可以通过这个好友链接访问 😃
www.learnml.wiki/forward-and-backward-mapping-for-computer-vision/
本文将介绍和解释两种图像扭曲算法:前向映射和后向映射。除了在理论层面介绍这些算法之外,还将它们应用于实际图像,以查看每种算法的结果和能力。
为了充分理解本文解释的内容,需要熟悉 2D 变换矩阵,这些内容在上一篇文章中进行了介绍和解释。
通过变换矩阵进行计算机视觉中的缩放、旋转和平移
介绍
如前一篇文章所示,对图像应用变换的方法是遍历图像的每个像素,并对每个像素单独应用变换。然而,在某些用例中,变换不能直接应用,例如,某些像素的新位置可能超出了图像范围。另一个可能的问题是新图像可能会有空白像素(白条),因为在变换后很难将原始图像的所有像素映射到新图像的所有像素。
为了避免这些问题,本文将介绍的两个算法,前向映射和后向映射,采用了不同的技术来正确转换图像。
前向映射
前向映射过程包括在引言和上一篇文章中讨论的简单图像变换过程:它遍历图像的所有像素,并对每个像素单独应用相应的变换。然而,必须考虑到那些变换后像素的新位置超出图像范围的情况,如下例所示。
转换后的图像像素超出了原始图像的范围。图像由作者提供
要执行前向映射过程,首先定义一个函数,该函数以像素的原始坐标作为参数。这个函数会对原始像素坐标应用转换,并返回转换后的新坐标。以下代码示例展示了旋转变换的函数。
def apply_transformation(original_x: int, original_y: int) -> Tuple[int, int]:
# Define the rotation matrix
rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],
[np.sin(np.pi/4), np.cos(np.pi/4), 0],
[0, 0, 1]])
# Apply transformation after setting homogenous coordinate to 1 for the original vector.
new_coordinates = rotate_transformation @ np.array([original_x, original_y, 1]).T
# Round the new coordinates to the nearest pixel
return int(np.rint(new_coordinates[0])), int(np.rint(new_coordinates[1]))
一旦你拥有这个函数,你只需遍历图像的每个像素,应用变换,并检查新像素坐标是否在原始图像的范围内。如果新坐标在范围内,新图像的新坐标上的像素将采用原始图像中原始像素的值。如果超出图像范围,该像素将被忽略。
def forward_mapping(original_image: np.ndarray) -> np.ndarray:
# Create the new image with same shape as the original one
new_image = np.zeros_like(original_image)
for original_y in range(original_image.shape[1]):
for original_x in range(original_image.shape[0]):
# Apply rotation on the original pixel's coordinates
new_x, new_y = apply_transformation(original_x, original_y)
# Check if new coordinates fall inside the image's domain
if 0 <= new_y < new_image.shape[1] and 0 <= new_x < new_image.shape[0]:
new_image[new_x, new_y, :] = original_image[original_x, original_y, :]
return new_image
旋转变换应用前向映射的结果可以在下图中看到,左侧是原始图像,右侧是转换后的图像。需要注意的是,对于这张图像,坐标原点在左上角,因此图像围绕该点逆时针旋转。
应用前向映射的结果。左图摘自 MNIST 数据集 [1]。完整图像由作者提供
关于变换结果,可以看到变换后的图像不像原始图像那样有完全黑色的背景,而是有许多白色条纹。正如引言中提到的,这种情况发生是因为原始图像的像素并不总是映射到新图像的所有像素上。由于新坐标是通过四舍五入到最近的像素计算的,这导致许多中间像素没有获得值。在这种情况下,由于新图像初始化时所有像素为空,因此在变换过程中没有赋值的像素将保持为空,生成了变换图像中的白色条纹。
此外,还应注意另一个显著问题:重叠。当原始图像的两个像素被变换到新图像的同一像素时,就会出现这个问题。对于本文使用的代码,如果有两个原始图像的像素映射到新图像的同一像素,新像素将取最后一个已变换的原始像素的值,覆盖第一个已设置的值。
反向映射
反向映射算法产生的原因是需要消除由于变换而在图像中生成的白色条纹以及可能的重叠。正如前面所提到的,这些条纹出现的原因是由于在前向映射过程中计算新坐标时四舍五入的缘故,导致不是所有的变换图像像素都有值,而重叠发生在原始图像的两个或多个像素映射到新图像的同一像素时。
这个算法背后的逻辑很简单:不是将原始图像的每个像素转换到新图像中的新坐标(前向),而是将新图像中的所有像素逆向转换到原始图像的像素(反向)。这样,新图像中就不会有没有值的像素,因为它们都会采用原始图像中单个像素的值,从而解决了两个问题。
幸运的是,使用变换矩阵对像素坐标进行的变换可以通过应用逆变换矩阵来撤销。变换矩阵的这个属性以及其证明可以在下图中看到。
使用逆变换矩阵的变换及证明。作者提供的图像
考虑到这一属性,算法的组成包括对新图像的每个像素进行迭代,并对每个像素的坐标应用逆变换,以知道它们需要从原始图像的哪个像素获取值。
def apply_inverse_transformation(new_x: int, new_y: int) -> Tuple[int, int]:
# Define the inverse rotation matrix
rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],
[np.sin(np.pi/4), np.cos(np.pi/4), 0],
[0, 0, 1]])
inverse_rotate_transformation = np.linalg.inv(rotate_transformation)
# Apply transformation after setting homogenous coordinate to 1 for the position vector.
original_coordinates = inverse_rotate_transformation @ np.array([new_x, new_y, 1]).T
# Round the original coordinates to the nearest pixel
return int(np.rint(original_coordinates[0])), int(np.rint(original_coordinates[1]))
注意,apply_inverse_transformation() 函数接受新图像中的坐标作为输入,并返回原始图像中的坐标,而不是像正向映射那样接受原始坐标并返回新坐标。
def backward_mapping(original_image: np.ndarray) -> np.ndarray:
# Create the new image with same shape as the original one
new_image = np.zeros_like(original_image)
for new_y in range(new_image.shape[1]):
for new_x in range(new_image.shape[0]):
# Apply inverse rotation on the new pixel's coordinates
original_x, original_y = apply_inverse_transformation(new_x, new_y)
# Check if original coordinates fall inside the image's domain
if 0 <= original_y < original_image.shape[1] and 0 <= original_x < original_image.shape[0]:
new_image[new_x, new_y, :] = original_image[original_x, original_y, :]
return new_image
应用反向映射的旋转变换结果可以在下图中看到,左侧是原始图像,右侧是变换后的图像。如前所述,图像围绕坐标原点旋转,坐标原点位于左上角。
反向映射的应用结果。左侧图像摘自 MNIST 数据集 [1]。完整图像由作者提供
在图像中你可以看到,应用正向映射时出现的那些白色条纹,在应用反向映射时都消失了。实际上,你可以看到变换后的图像质量相当好(我们必须考虑到原始图像的质量不是很高),因此我们可以认为,在出现白色条纹的情况下,反向映射算法明显优于正向映射。
结论
正向映射是一个简单易实现且易于理解的算法,因为它直接将原始图像的每个像素转换到新图像。然而,该算法存在重叠问题以及留下一些像素没有值的问题,这些问题会显著降低变换图像的质量。反向映射算法,其实现与正向映射一样简单,结果要好得多,并且解决了这两个问题,因为它为新图像的所有像素提供了一个唯一的值。
关于算法的执行时间,两者具有相同的复杂度,因此一般来说,使用反向映射算法通常是一个更好的选择,因为它的结果更佳。在理想情况下,负责对每个像素应用个体变换的函数(在本文中称为apply_transformation()和apply_inverse_transformation())不会构造变换矩阵,而是将其作为参数传入。这将节省正向映射算法构建变换矩阵所需的执行时间,同时也节省反向映射算法构建和反转矩阵的时间。
总之,反向映射算法相比于正向映射算法取得了非常好的结果,两者的执行时间几乎相同。然而,需要注意的是,两种算法都需要较长时间来处理高分辨率图像,尽管如此,它们仍然非常有用,为构建其他更强大的变换算法奠定了基础。
数据
本文中使用的图像摘自 MNIST 数据集 [1]。该数据集在创作共用署名-相同方式共享 3.0 许可协议下提供。
参考资料
[1] yann.lecun.com/exdb/mnist/
神经网络中的前向传播与反向传播
原文:
towardsdatascience.com/forward-pass-backpropagation-neural-networks-101-3a75996ada3b
解释神经网络如何通过手动和代码中的 PyTorch 进行“训练”和“学习”数据模式
·发表于 Towards Data Science ·10 分钟阅读·2023 年 11 月 4 日
--
神经网络图标由 juicy_fish 创建 — Flaticon. www.flaticon.com/free-icons/neural-network
.
背景
在我过去的两篇文章中,我们探讨了神经网络从单一的 感知机到大型互联的 (多层感知机 (MLP) ) 非线性优化引擎的起源。如果你对感知机、MLP 和激活函数不熟悉,我强烈建议你查看我之前的帖子,因为我们在这篇文章中将讨论很多:
神经网络及其构建块的介绍
levelup.gitconnected.com](https://levelup.gitconnected.com/intro-perceptron-architecture-neural-networks-101-2a487062810c?source=post_page-----3a75996ada3b--------------------------------) ## 激活函数与非线性:神经网络 101
解释为什么神经网络可以学习(几乎)任何事物
towardsdatascience.com
现在是时候理解这些神经网络如何“训练”和“学习”你传递给它的数据中的模式了。有两个关键组件:前向传播 和 反向传播。让我们深入了解吧!
架构
让我们快速回顾一下神经网络的一般结构:
一个基本的两层隐藏的多层感知器。图示由作者提供。
每个隐藏神经元执行以下过程:
每个神经元内部执行的过程。图示由作者提供。
-
输入: 这些是我们数据的特征。
-
权重: 我们乘以输入的一些系数。算法的目标是找到最优的权重。
-
线性加权和: 将输入和权重的乘积加起来,并添加一个偏置/偏移项, b*。
-
隐藏层: 这是存储多个神经元以学习数据模式的地方。上标指的是层,下标指的是该层中的神经元/感知器。
-
边缘/箭头: 这些是网络的权重,从相应的输入中获取,无论是特征还是隐藏层输出。我已经省略它们以使图表更简洁。
前向传播
概述
训练神经网络的第一部分是让它生成预测。这被称为 前向传播,即数据从第一层经过所有神经元到最后一层(也称为输出层)。
对于本文,我们将手动进行前向传播。实际上,我们会使用像 PyTorch 或 TensorFlow*** 的包。但这将帮助我们更好地理解过程。
示例神经网络
我们将执行前向传播的神经网络如下所示:
简单的神经网络。图示由作者提供。
如你所见,使用两个输入、隐藏层中的两个神经元(具有 ReLU 激活函数)和一个预测(输出层)非常简单。
权重和偏置
现在我们为这个简单的网络创建一些任意的权重、偏置和输入:
输入值: [0.9, 1.0]
目标/真实值: [2.0]
输入到隐藏层的权重,W_1:
-
神经元 1: W_{1,1} = [0.2, 0.3]
-
神经元 2: W_{1,2} = [0.4, 0.5]
隐藏层偏置,b_1: [0.1, 0.2]
隐藏到输出层的权重, W_2: [0.5, 0.6]
输出层偏置,b_2: [0.4]
需要注意的是,在这种情况下我随机生成了初始权重和偏置。这并不是坏事,你可以通过完全随机的初始化获得不错的结果。然而,还有更复杂的方法:
-
Xavier: 适用于 sigmoid 和 tanh 激活函数。它从均匀分布中生成随机权重,利用该节点的输入数量来设置分布范围。
-
He: 适用于 ReLU 激活函数。它从正态分布中生成随机权重,利用该节点的输入数量来设置标准差。
如果你想了解更多关于这些初始化方法的信息,请查看上面列表中的链接。
关于权重初始化还有一个重要的注意事项是确保它们不同,以便我们‘打破对称性。’ 如果同一层的神经元以相同的权重开始,它们很可能会被完全相同地更新。因此,网络将不会收敛,无法学习任何东西。
第一次前向传播
使用上面列出的数据,我们现在可以进行第一次前向传播!结果如下:
从输入到隐藏层的线性加权和,z¹_{1} 和 z¹_{2},是:
由作者在 LaTeX 中生成的方程。
记住,上标表示层,下标表示该层中的神经元。第一层被认为是紧接着输入层的那一层,因为输入层不进行任何计算。因此,在我们的情况下,我们有一个 2 层的网络。
在这里,我们利用了 点积 来简化和压缩计算。 现在,我们可以在隐藏层中执行 ReLU 激活函数,a¹_{1,1} 和 a¹_{1,2}:
由作者在 LaTeX 中生成的方程。
我们需要做的最后一步是生成输出, z²_{1},这没有关联的激活函数:
由作者在 LaTeX 中生成的方程。
哇,我们刚刚完成了第一次前向传播!
注意:我通过将权重写为向量/矩阵来简化表达。这是文献中常见的做法,以使工作流程更加整洁。
前向传播也如下图所示:
简单神经网络及其权重、偏置和输出。图由作者提供。
反向传播算法
概述
完成正向传播后,我们现在可以开始更新权重和偏置,以最小化网络预测的误差。权重和偏置的更新是通过 反向传播 算法实现的。
计算图、链式法则和梯度下降
现在,让我们理解一下这个算法的直观意义。反向传播旨在获得每个权重和偏置对误差(损失)的偏导数。然后使用 梯度下降 更新每个参数,以最小化由每个参数引起的误差(损失)。
对,这在纸面上可能有意义,但我理解它仍然可能显得有些任意。我想通过一个真正的“简单”示例来深入了解 计算图。
考虑以下函数:
由作者在 LaTeX 中生成的方程。
我们可以将其绘制为计算图,这只是另一种可视化计算的方法:
计算图示例。图示由作者提供。
基本上这是一个计算 f(x,y,z) 的流程图。我也表达了 p=x-y。现在我们来插入一些数字:
带有数字的计算图示例。图示由作者提供。
这些看起来到目前为止都很好且直观!
我们计算 f(x,y,z) 最小值的方法是使用微积分。特别是,我们需要知道 f(x,y,z) 对其所有三个变量 x, y, 和 z 的偏导数。
我们可以开始计算 p=x-y 和 f=pz 的偏导数:
由作者在 LaTeX 中生成的方程。
但是,我们该如何进行呢?
由作者在 LaTeX 中生成的方程。
好吧,我们使用 链式法则! 这是一个 x 的示例:
由作者在 LaTeX 中生成的方程。
通过组合不同的偏导数,我们可以得到我们期望的表达式。因此,对于上述示例:
由作者在 LaTeX 中生成的方程。
输出的梯度 f 对 x 的梯度是 z。这很有意义,因为 z 是我们唯一乘以 x 的值。
对 y 和 z 进行重复:
由作者在 LaTeX 中生成的方程。
现在,我们可以在计算图上写出这些梯度及其对应的值:
带有数字和梯度的计算图示例。图示由作者提供。
梯度下降通过在梯度的相反方向上小幅度更新值(x, y, z)来工作。梯度下降的目标是尽量减少输出函数。例如,对于 x:
作者生成的 LaTeX 方程式。
其中 h 称为 学习率,决定了参数更新的幅度。在这个例子中,让我们定义 h=0.1,所以 x=3.7。现在的输出是什么?
执行梯度下降后的计算图示例,包含数字和梯度。图示由作者提供。
输出变小了,换句话说,它正在被最小化!
这个例子灵感来源于 Andrej Karpathy 在 YouTube 上的视频。我强烈推荐你查看,以深入了解这一过程。
应用反向传播
以上所有内容如何与神经网络的训练相关?
好了,神经网络其实就是一个计算图!输入类似于上面的 x, y 和 z,而操作门类似于神经元中的激活函数。
现在,让我们将这一过程应用于我们之前的简单示例神经网络。记住,我们的预测是 1.326,假设目标是 2.0。这个预测中的损失函数(误差)可以是 均方误差:
作者生成的 LaTeX 方程式。
记住附加在 z 上的 2 是层数,而不是幂次项!
下一步是计算相对于预测/输出的损失梯度:
作者生成的 LaTeX 方程式。
现在,我们需要计算相对于输出层偏置和权重的损失梯度:W_2 和 b_2:
作者生成的 LaTeX 方程式。
如果上述表达式看起来让你感到害怕,不必担心。我们所做的只是进行了偏导数计算,并多次应用了链式法则。
作者生成的 LaTeX 方程式。
最后一步是使用梯度下降更新参数,其中我设置了学习率 h=0.1:
作者生成的 LaTeX 方程式。
Voilà,我们已经更新了输出层的权重和偏置!
接下来是对隐藏层的权重和偏置重复这一过程。首先,我们需要找到相对于激活函数输出的损失,a¹_{1,1} 和 a¹_{1,2}:
由作者在 LaTeX 中生成的方程。
现在,我们找到 z¹_{1} 和 z¹_{2} 的导数:
由作者在 LaTeX 中生成的方程。
一些背景信息:
由作者在 LaTeX 中生成的方程。
下一步是找到相对于 W_{1,1} 和 W_{1,2} 的损失。在此之前,我想重新定义一些符号,以便清楚我们在处理哪个权重:
由作者在 LaTeX 中生成的方程。
因此:
由作者在 LaTeX 中生成的方程。
因此,相对于权重和偏置的损失的偏导数是:
由作者在 LaTeX 中生成的方程。
最后一步是使用梯度下降来更新这些值:
由作者在 LaTeX 中生成的方程。
哇,我们刚刚完成了整个反向传播的迭代!
一个前向传播和反向传播(反向传播)的周期被称为 epoch。
下一步是使用更新后的权重和偏置进行另一次前向传播。这个前向传播的结果将是 1.618296。这接近目标值 2,因此网络已经“学习”到了更好的权重和偏置。这就是“机器学习”在发挥作用。真的很惊人!
实际上,这个算法会运行 100 或 1000 次 epoch。幸运的是,已经存在处理此过程的包,因为手动完成会非常繁琐!
额外细节
可以看出,为什么这个过程被称为反向传播,因为我们在网络的每一层向后传播误差(导数)。一旦你掌握了它,理解起来就很简单。因此,我强烈建议你慢慢地进行这个过程,最好自己动手,我保证你会很快理解!
PyTorch 示例
手动完成,如上所述,确实很耗费精力,而且这只是一个小网络。然而,我们可以利用 PyTorch(一个深度学习库)来为我们完成所有这些繁重的工作:
GitHub Gist 由作者提供。
因此,我们手动编写的整个前向传播和反向传播过程可以在 ~50 行代码中完成!Python 的威力!
然而,由于随机化、浮点精度和其他计算机因素,权重、偏置和预测可能与手动计算的结果不完全匹配。这不是问题,但重要的是要意识到这一点。
如果你想了解更多关于 PyTorch 的内容,可以查看他们网站上的 教程。
总结与进一步思考
在本文中,我们探讨了神经网络如何生成预测并从错误中学习。这个过程围绕使用部分微分更新网络的参数,针对损失误差。算法是反向传播,因为它通过链式法则将误差反向传播通过每一层。一旦你掌握了它,反向传播相当直观,但对于大型神经网络来说非常繁琐。这就是我们使用像 PyTorch 这样的深度学习库,它为我们做了大部分繁重的工作。
本文中使用的完整代码可以在我的 GitHub 上找到:
[## Medium-Articles/Neural Networks/basic_foward_backward_pass.py 在主分支 · egorhowell/Medium-Articles
我在我的中等博客/文章中使用的代码。通过在…上创建帐户来贡献 egorhowell/Medium-Articles 的开发。
参考资料与进一步阅读
-
另一个手动训练神经网络的例子
另一件事!
我有一个免费的新闻通讯,数据解析,在其中我分享成为更优秀数据科学家的每周技巧。没有“虚华”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作的见解。
[## 数据解析 | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读《数据解析》,由 Egor Howell 撰写,这是一个 Substack 出版物,内容包括…
newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----3a75996ada3b--------------------------------)
与我联系!
四种构建在生成式人工智能基础模型之上的方法
每种方法的优缺点、有效性以及示例代码
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 3 月 21 日
--
如果我在这里使用的术语对你来说不熟悉,我鼓励你先阅读我的 关于大型语言模型的早期文章 。
确实有团队在实际应用中使用 ChatGPT 或其竞争者(如 Anthropic、Google 的 Flan T5 或 PaLM、Meta 的 LLaMA、Cohere、AI21Labs 等),而不是仅仅用于炫酷的演示。不幸的是,关于他们如何做到这一点的信息在市场营销噱头和技术术语中被掩盖。因此,我看到刚开始接触生成式 AI 的人们采取了一些领域专家会告诉你不会奏效的方法。本文尝试组织这个领域并展示哪些方法有效。
要跨越的标准
许多关于生成式 AI 的炫酷演示和充满噱头的帖子存在的问题在于它们 击中了训练数据集 ——它们并没有真正告诉你在应用于真实用户和实际新输入的混乱情况下效果如何。典型软件预计要达到 99% 以上的可靠性 ——例如,只有当语音识别技术在短语上跨越了这一准确度标准时,语音 AI 市场才得以起飞。自动字幕、翻译等情况也是如此。
我看到团队在其生产系统中解决这一问题有两种方式:
-
如果用户体验在他们已经预期需要纠正错误的情况下,人类用户会更宽容(这似乎是 GitHub Copilot 帮助的原因),或者如果它被定位为互动和有帮助但尚未准备好使用(如 ChatGPT、Bing Chat 等)。
-
目前,生成式 AI 的完全自动化应用大多处于受信测试阶段,是否能够真正达到这一标准仍有待观察。尽管如此,结果令人鼓舞,并且趋势向上发展,最终达到标准只是时间问题。
就个人而言,我一直在尝试 GPT 3.5 Turbo 和 Google Flan-T5,并针对特定的生产使用案例进行实验,并了解了许多有效和无效的方面。我的模型都没有跨越 99% 的标准。到写作时(2023 年 3 月),我也尚未获得 GPT-4 或 Google 的 PaLM API 的访问权限。我基于我的实验、已发布的研究和公开宣布的项目撰写了这篇文章。
在所有生成式 AI 的应用中,牢记预训练模型是基于互联网内容进行训练的,并可能存在多种偏见。这些偏见在你的应用层面上要加以防范。
方法 1:直接使用 API
第一个方法是最简单的,因为许多用户通过 ChatGPT 提供的交互式界面遇到了 GPT。尝试各种提示直到得到你想要的输出似乎非常直观。这就是为什么你会看到许多 LinkedIn 影响者发布了 适用于销售邮件的 ChatGPT 提示 或其他内容。
当涉及到自动化这个工作流时,自然的方法是使用服务的 REST API 端点 并用最终有效的提示直接调用它:
import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
openai.Edit.create(
model="text-davinci-edit-001",
input="It was so great to meet you .... ",
instruction="Summarize the text below in the form of an email that is 5 sentences or less."
)
然而,这种方法不适合操作化。原因有几个:
-
脆弱性。底层模型持续改进。部署模型的突然变化 破坏了 许多生产工作负载,人们从这些经验中学到了很多。机器学习工作负载已经很脆弱了;在特定模型的提示中添加额外的失败点是不明智的。
-
注入。像上述示例中那样,指令和输入通常不是简单的字符串。它们大多包括来自用户的变量。这些变量必须被纳入提示和输入中。正如任何程序员所知道的那样,通过字符串连接进行注入充满了安全问题。当你这样做时,你将自己置于生成式 AI API 周围的保护措施的摆布之下。像防御 SQL 注入一样,使用一个处理变量注入的 API 更为妥当。
-
多个提示。很少能在一次尝试中让一个提示奏效。更常见的做法是向模型发送多个提示,并让模型根据这些提示修改其输出。这些提示本身可能会在工作流中嵌入一些人工输入(例如后续输入)。提示提供一些期望输出的示例(称为少量示例学习)也是很常见的。
解决这三个问题的一种方法是使用 langchain。
方法二:使用 langchain
Langchain 正迅速成为一个首选库,允许你调用来自不同供应商的 LLM,处理变量注入,并进行少量示例训练。 这是一个使用 langchain 的示例:
from langchain.prompts.few_shot import FewShotPromptTemplate
examples = [
{
"question": "Who lived longer, Muhammad Ali or Alan Turing?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
"""
},
{
"question": "When was the founder of craigslist born?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952
"""
...
]
example_prompt = PromptTemplate(input_variables=["question", "answer"],
template="Question: {question}\n{answer}")
prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
suffix="Question: {input}",
input_variables=["input"]
)
print(prompt.format(input="Who was the father of Mary Ball Washington?"))
我强烈推荐使用 langchain 而不是直接使用供应商的 API。然后,确保你所做的一切与至少两个 API 兼容,或者使用一个不会在你使用期间改变的 LLM 检查点。这两种方法都能避免你的提示/代码对底层 LLM 的变化过于脆弱。(这里,我使用 API 指代一个托管的 LLM 端点)。
Langchain 目前 支持来自 Open AI、Cohere、HuggingFace Hub(因此包括 Google Flan-T5)等的 API,以及 来自 AI21、Anthropic、Open AI、HuggingFace Hub 等的 LLM。
方法 3:微调生成 AI 链
这是前沿的方法,因为这是我看到大多数复杂生成 AI 生产应用程序所使用的方法。仅举一个例子(并非 endorsement),微调就是 一家由斯坦福博士组成的初创公司如何接近像 SQL 生成和记录匹配这样的标准企业用例。
要理解这种方法背后的理由,了解支撑 ChatGPT(或其竞争者)的四个机器学习模型是有帮助的:
-
大型语言模型(LLM)被训练以预测给定前一个词后的下一个词。它通过学习在大量文档中的词汇关联和模式来实现这一点。模型足够大,可以在不同的上下文中学习这些模式。
-
基于人类反馈的强化学习模型(RL-HF)通过向人类展示生成的文本示例,并要求他们批准令人愉悦的文本来进行训练。之所以需要这样做,是因为 LLM 的输出是概率性的——它不会预测一个单一的下一个词,而是预测一组词,每个词都有一定的概率会出现。RL-HF 使用人类反馈来学习如何选择生成对人类有吸引力的文本的延续。
-
指令模型是一个监督模型,通过显示提示(“生成一封向工程领导提议演示的销售邮件”)并在销售邮件的示例上训练模型来进行训练。
-
上下文模型被训练以与用户进行对话,使他们能够通过连续的提示来制定输出。
此外,还有保护机制(对输入和输出的过滤器)。模型会拒绝回答某些类型的问题,并撤回某些答案。在实际应用中,这些都是不断更新的机器学习模型。
步骤 2:RL-HF 如何工作。图片来自 Stiennon et al, 2020
有一些开源生成 AI 模型(Meta 的 LLaMA,Google 的 Flan-T5)允许您从上述任意步骤开始(例如,使用发布的检查点中的步骤 1-2,自行在您的数据上训练第 3 步,不执行第 4 步)。请注意,LLaMA 不允许商业使用,而 Flan-T5 已经有一年时间(因此您需要在质量上做出妥协)。了解每个阶段的成本/收益有助于您决定从哪里开始。
-
如果您的应用程序使用的术语和词汇非常不同,可能有必要从头开始在您的数据上构建一个 LLM(即,从第 1 步开始)。问题是您可能没有足够的数据,即使有足够的数据,训练成本也会很高(每次训练运行的费用在 300 万到 500 万美元之间)。这似乎是 Salesforce 对 他们为开发人员使用的生成 AI 所做的。
-
RL-HF 模型经过训练,旨在吸引那些可能不是主题专家或代表您自己用户的测试人员。如果您的应用程序需要主题专业知识,您可能更适合从 LLM 开始,然后从第 2 步分支出去。所需的数据集要小得多——Stiennon 等人在 2020 年使用了 125k 文档,并在每次迭代中为每个输入文档呈现一对输出(见图示)。因此,您需要人类标注者随时待命,以对约 100 万个输出进行评分。假设一个标注者需要 10 分钟来评分每对文档,那么每次训练的人工成本大约是 250 人月。我估计费用在 25 万美元到 200 万美元之间,具体取决于地点和技能水平。
-
ChatGPT 被训练来响应成千上万的不同提示。另一方面,您的应用程序可能只需要一两个特定的提示。将 Google Flan-T5 训练到您的特定指令和输入上可能会很方便。这样的模型可以更小(因此部署成本更低)。这种服务成本上的优势解释了为什么第 3 步是最常见的分支点。您可以使用 HuggingFace 和/或 Keras 对 Google Flan-T5 进行针对您特定任务的微调。您可以在常用的 ML 框架如 Databricks、Sagemaker 或 Vertex AI 上进行,并使用相同的服务来部署训练后的模型。由于 Flan-T5 是 Google 的模型,GCP 通过在 Vertex AI 中提供 预构建容器 使训练和部署变得非常简单。费用可能在 50 美元左右。
-
理论上,有可能通过不同的方法来保持对话上下文。然而,我在实际应用中没有看到这种做法。大多数人做的是使用像 Dialogflow 这样的对话代理框架,它已经内置了 LLM,并为他们的应用设计一个定制的聊天机器人。基础设施成本微不足道,你只需要领域知识,无需任何 AI 专业知识。
可以在这些阶段中的任何一个进行中断。以下例子仅限于公开发布的医学研究:
-
这篇Nature 文章从从医学记录中提取的 90 亿字中构建了一个自定义的 89 亿参数 LLM(即,它们从第 1 步开始)。作为比较,下文第 3 种方法中使用的 Flan-PaLM 具有5400 亿参数,而“小型/高效”PaLM 为 620 亿参数。显然,成本是定制语言模型变得更大的一个限制因素。
-
这项MIT CSAIL 研究要求模型严格遵循现有文本,并进行指令微调(即,它们从第 2 步开始)。
-
Deep Mind 的 MedPaLM从一种指令调优的 PaLM 变体 Flan-PaLM 开始(即,它从第 3 步开始)。他们报告称,93%的医疗专业人员认为该 AI 的回答与人类回答相当。
我的建议是,根据你的应用领域与基础模型训练所用的通用互联网文本的差异,选择中断的位置。你应该微调哪个模型?目前,Google Flan T5 是最先进的可微调模型,且开放用于商业用途。对于非商业用途,Meta 的 LLaMA 是最先进的模型。
但需要注意的是:当你使用开源模型进行链式操作时,保护过滤器不会存在,因此你需要设置毒性安全措施。一个选项是使用detoxify库。确保在生产环境中的任何 API 端点周围都实施毒性过滤,否则你会发现自己不得不撤下它。API 网关可以是确保你对所有 ML 模型端点进行此操作的便捷方式。
方法 4:简化问题
有一些聪明的方法可以重新定义你所解决的问题,以便可以使用生成式 AI 模型(如第 3 种方法),但避免出现幻觉等问题。
例如,假设你想做问答系统。你可以从一个强大的 LLM 开始,然后努力“驯服”这个“野兽”,使其不出现幻觉。一个更简单的方法是重新定义问题。将模型从一个预测输出文本的模型改为一个有三个输出的模型:文档的 URL、文档中的起始位置和文本长度。这就是谷歌搜索在这里做的:
谷歌的问答模型预测一个 URL、起始位置和文本长度。这避免了幻觉问题。
最糟糕的情况下,模型会显示不相关的文本。它不会出现幻觉,因为你不允许它真正预测文本。
一个 遵循此方法的 Keras 示例 将输入和上下文(你要在其中找到答案的文档)进行标记化:
from transformers import AutoTokenizer
model_checkpoint = "google/flan-t5-small"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
...
examples["question"] = [q.lstrip() for q in examples["question"]]
examples["context"] = [c.lstrip() for c in examples["context"]]
tokenized_examples = tokenizer(
examples["question"],
examples["context"],
...
)
...
然后将这些令牌传递给一个 Keras 回归模型,其第一层是 Transformer 模型,该模型接收这些令牌并输出答案在“上下文”文本中的位置:
from transformers import TFAutoModelForQuestionAnswering
import tensorflow as tf
from tensorflow import keras
model = TFAutoModelForQuestionAnswering.from_pretrained(model_checkpoint)
optimizer = keras.optimizers.Adam(learning_rate=5e-5)
model.compile(optimizer=optimizer)
model.fit(train_set, validation_data=validation_set, epochs=1)
在推理过程中,你会得到预测的位置:
inputs = tokenizer([context], [question], return_tensors="np")
outputs = model(inputs)
start_position = tf.argmax(outputs.start_logits, axis=1)
end_position = tf.argmax(outputs.end_logits, axis=1)
你会注意到,样本并没有预测 URL——上下文假定是典型搜索查询的结果(例如由匹配引擎或向量数据库返回),样本模型只进行提取。然而,你可以通过在 Keras 中添加一个单独的层来将搜索功能也构建到模型中。
总结
我见到的四种在生成 AI 基础模型之上构建生产应用程序的方法:
-
使用像 GPT-4 这样的全能模型的 REST API 进行单次提示。
-
使用 langchain 来抽象化 LLM、输入注入、多轮对话和少样本学习。
-
利用一组模型来对你的自定义数据进行微调,这些模型构成了一个端到端的生成 AI 模型。
-
将问题重新定义为一种避免生成 AI(偏见、毒性、幻觉)风险的形式。
方法 #3 是我见过的最常被成熟团队使用的方法。
A/B 测试中的四个常见错误及解决方法
原文:
towardsdatascience.com/four-common-mistakes-when-a-b-testing-and-how-to-solve-them-384072b57d75
提升你的 A/B 测试技能:解决四个关键错误以获得更好的结果
·发布于 Towards Data Science ·7 分钟阅读·2023 年 6 月 29 日
--
图片来源 Oscar Ivan Esquivel Arteaga 在 Unsplash
A/B 测试就像 Jenga,一种由相互关联的部分组成的微妙平衡,形成了成功实验的基础。就像在 Jenga 游戏中,移除错误的块可能会导致整个塔崩溃一样,A/B 测试依赖于多个组件的共同工作。每个部分代表测试的一个关键元素,如果其中任何一个失败了,实验的完整性可能会受到威胁,导致不准确的结果或错失机会。
在我的经验中,我见证了许多出色的实验创意因为许多数据科学家,包括我自己,犯的常见错误而失败!因此,我想和你探讨四个在 A/B 测试中最常见的错误(以及如何解决这些错误!)。
如果你对 A/B 测试不熟悉,并且有意从事数据科学职业,我强烈建议你至少熟悉一下这个概念。
如果你想了解 A/B 测试的入门知识,可以查看我的下文:
这是数据科学家最重要的统计方法之一
towardsdatascience.com
说到这里,让我们深入探讨吧!
问题 #1:统计功效设定得过低。
总结一下,统计功效表示正确检测真实效应的概率,或者更准确地说,它是在零假设为假时拒绝零假设的条件概率。统计功效与犯第二类错误(假阴性)的概率成反比。
通常,在进行研究时,将功效设置为 80% 是一种常见做法。根据其定义,这意味着如果你将功效设置为 80%,那么当零假设为假时,你将有 20% 的概率未能拒绝零假设。简单来说,如果在 100 次实验中存在真实效应,你只会检测到 80 次。
这为什么是个问题?
在商业中,尤其是在科技公司中,一个主要目标是尽可能快速地学习、构建和迭代。大型科技公司,如 Airbnb 和 Uber,能够如此快速地增长并维持市场份额,其中一个原因就是它们能够不断进化。
当你将统计功效设置为 80% 时,这意味着 20% 的真实效应没有被捕捉到,也就是说少了 20% 的迭代。现在将这个问题扩大到,比如说 10 年,你就能理解这可能产生的影响。
解决方案是什么?
对此显而易见的解决办法是提高统计功效。而如何做到这一点并不是那么明显——统计功效直接与其他几个实验参数相关,这意味着有几种方法可以提高统计功效:
-
增加样本大小。提高统计功效的主要方法是增加样本大小。通过增加样本大小,数据的变异性减少,从而导致更窄的置信区间和更准确的估计。这就是为什么如果你使用像 Evan Miller 的 A/B 测试计算器这样的功效分析工具,设置更高的功效会导致推荐的样本大小更大。
-
调整 alpha 值。功效和 alpha 值是反相关的,这一点如果你仔细考虑就会明白。如果你将 alpha 值从 0.05 降低到 0.01,拒绝零假设的阈值会变得更严格,从而使得拒绝变得更加困难,这会导致统计功效降低。反之亦然——如果你的 alpha 值过低,通过增加 alpha 值,你更可能拒绝零假设,并获得更高的统计功效。
-
减少效应大小(最小可检测效应)。通过减少效应大小,测试的统计功效会增强,因为这会增加检测较小效应的能力。不过,这并不是总是推荐的做法,这引出了我的下一个要点!
问题 #2:将最小可检测效应(MDE)设置得过低。
最小可检测效应(MDE)代表实验可以可靠检测到的最小效应大小。如果观察到的效应大小低于 MDE,说明该效应如此微小,以至于可能只是由于数据中的随机变异或噪音,而不是实际的有意义的效应。
如果是这样的话,那为什么不希望将 MDE 设定得尽可能低呢?这就是统计显著性与实际显著性的概念发挥作用的地方——统计显著性关注的是效应不是由于随机机会的概率,而实际显著性则考虑了效应在实际中的大小和影响。
举个例子,我在 KOHO 进行了一项定价实验,以确定某一产品的价格弹性。最终结果在统计上显著,因为价格的降低导致了产品采纳的增加。然而,我们降低价格的幅度并不实际显著,因为尽管用户数量增加了,但价格的降低最终导致了总体利润的减少。
解决方案是什么?
你应该根据与上下文相关的实际效应大小来选择最小可检测效应(MDE),并使其与实验的目标保持一致。这确保了检测到的效应既在统计上显著,又在实际中有意义,同时优化资源分配,避免假阴性风险。
问题 #3:进行过多的假设检验。
我经常看到进行几十次 A/B 测试(假设检验)来测试几乎相同的内容,比如测试某一产品的多个价格点、测试各种网站配置和测试多个营销活动。
为什么这是个问题?
问题在于,当你进行更多的假设检验时,你更有可能偶然获得统计显著的结果。从统计学角度看,这是因为 A/B 测试时设定的 alpha 值。alpha 代表了在原假设为真的情况下拒绝原假设的概率,因此如果我们将 alpha 设为 0.05,那么 100 个真实测试的假设中有 5 个将被拒绝。
解决方案是什么?
解决方案是控制虚假发现(假阳性),有几种方法可以实现这一点。最常见的技术是Bonferroni 校正,它通过将显著性水平(alpha)除以测试的数量来进行调整。例如,如果你进行 10 次假设检验并希望保持整体 alpha 为 0.05,你需要将 0.05 除以 10,从而为每个测试得到调整后的 alpha 值 0.005。此校正确保了更严格的统计显著性标准,减少了假阳性的可能性。
问题 #4:没有考虑幸存者偏差。
我看到的另一个问题是,实验设计中往往没有调整幸存者偏差。
这有什么问题?
幸存者偏差和用户任期之间有很强的关系。考虑这一点:不活跃或不盈利的用户不太可能与公司保持长期关系——那些没有从产品中获得价值的用户不太可能在公司停留太久。因此,考虑不同任期用户行为的潜在差异非常重要。
在拆分对照组和测试组时,如果未考虑用户任期,可能会因行为差异显著而影响结果。一组可能有更高的平均用户任期,这会影响盈利能力和参与度等因素。换句话说,未考虑用户任期可能引入混杂变量,并阻碍对感兴趣的特定因果关系的分析。
解决方案是什么?
分层抽样可以用来解决用户任期未控制而导致 A/B 测试结果偏差的问题。这包括将总体分成特定段,然后随机抽取每个组。可以通过以下步骤完成:
-
定义用户任期组:将用户群体按与公司关系的任期分成不同组。例如,你可以创建“新用户”(短期任期)、“中期用户”(中等任期)和“长期用户”(长期任期)等组。
-
确定样本大小:确定每个任期组所需的样本大小。样本大小可以与总体用户群体中每组的大小成比例,或基于特定考虑因素,例如每组的重要性或所需的统计功效。
-
在每个组内随机抽样:从每个任期组中随机选择用户,形成对照组和测试组。确保选择代表每组中的用户,保持不同任期级别用户的比例。
-
进行 A/B 测试:通过分层抽样,你将减少实验中的偏差,并为更可靠的结果做好准备。现在你可以以一种控制其他变量的方法来正确进行实验。
阅读完这些内容后,你应该知道四种常见的 A/B 测试错误及其解决方法——具体来说,你应该知道如何考虑:
-
将统计功效设得太低
-
将最小可检测效应(MDE)设得太低
-
进行过多的假设检验
-
未考虑幸存者偏差
考虑这些错误肯定会提高你 A/B 测试的有效性和可靠性,从而提供有意义的见解和信息化决策。
现在出去看看你能发现什么!
感谢阅读!
如果你喜欢这篇文章, 立即订阅并成为会员,以便不错过有关数据科学指南、技巧和窍门、生活经验等的任何文章!
[## 使用我的推荐链接加入 Medium - Terence Shin
解锁独家见解,保持领先:订阅体验知识的世界。发现一座宝藏...
terenceshin.medium.com](https://terenceshin.medium.com/membership?source=post_page-----384072b57d75--------------------------------)
不确定接下来读什么?我为你挑选了另一篇文章:
最受欢迎的机器学习模型的直观解释
terenceshin.medium.com](https://terenceshin.medium.com/all-machine-learning-algorithms-you-should-know-for-2023-843dba11419c?source=post_page-----384072b57d75--------------------------------)
或者你可以查看我的 Medium 页面:
阅读 Terence Shin 在 Medium 上的文章。数据科学 @ KOHO, Saturn Cloud | 硕士, MBA |…
terenceshin.medium.com](https://terenceshin.medium.com/?source=post_page-----384072b57d75--------------------------------)
Terence Shin
自 ChatGPT 以来的四个 LLM 趋势及其对 AI 构建者的影响
·
关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 5 月 29 日
--
表 1:截至 2023 年 5 月的热门 LLM(按提及数量排序)。提及数量、趋势和下游任务的适用性是从超过 50 万份与 AI 相关的在线文档中计算得出的,这些文档包括商业媒体、普通新闻、AI 博客和科学出版物。任务适用性通过语义嵌入和模型与 NLP 任务之间的潜在关联强度进行计算。
2022 年 10 月,我发布了一篇关于特定 NLP 用例的 LLM 选择的文章,涉及对话、翻译和摘要等内容。从那时起,AI 已经取得了巨大的进步,在这篇文章中,我们将回顾过去几个月的一些趋势以及它们对 AI 构建者的影响。具体而言,我们将涵盖自回归模型的任务选择、商业和开源 LLMs 之间不断变化的权衡,以及 LLM 的集成和生产中故障的缓解。
1. 生成 AI 推动了自回归模型,而自动编码模型则在等待它们的时机。
对于许多 AI 公司来说,ChatGPT 似乎已成为终极竞争者。在早期推介我的分析初创公司时,我经常面临挑战:“如果谷歌(Facebook、阿里巴巴、Yandex 等)突然出现并做同样的事情,你会怎么做?”现在,当前的问题是:“为什么你不能用 ChatGPT 来做这个?”
简短的回答是:ChatGPT 在很多方面表现出色,但远未覆盖 AI 的全部领域。目前的热潮主要集中在生成AI 上——而不是分析型 AI,或者其相对较新的分支——合成 AI[1]。这对 LLMs 意味着什么?正如我在上一篇文章中描述的,LLMs 可以通过三种目标进行预训练——自回归、自动编码和序列到序列(参见表 1,列“预训练目标”)。通常,一个模型会以这些目标中的一个进行预训练,但也有例外——例如,UniLM[2]在所有三种目标上进行了预训练。近几个月来使 AI 流行的有趣生成任务包括对话、问答和内容生成——这些任务中,模型确实学习“生成”下一个标记、句子等。这些任务最适合由自回归模型执行,包括 GPT 家族以及大多数近期的开源模型,如 MPT-7B、OPT 和 Pythia。自动编码模型更适合信息提取、蒸馏和其他分析任务,虽然它们处于背景中——但不要忘记,初次突破 LLM 的 2018 年是通过 BERT 这款自动编码模型实现的。虽然这对现代 AI 来说可能感觉像是石器时代,但自动编码模型对于许多 B2B 用例特别相关,其中重点是提炼出针对特定业务任务的简明见解。我们确实可能会看到围绕自动编码的另一波浪潮,以及一代新的 LLMs,在提取和综合信息以进行分析方面表现卓越。
对于构建者来说,这意味着流行的自回归模型可以用于所有内容生成的任务——内容越长,效果越好。然而,对于分析任务,你应该仔细评估所使用的自回归大型语言模型(LLM)是否能输出令人满意的结果,并考虑使用自动编码模型或更传统的自然语言处理方法。
2. 开源与商业公司的竞争,推动了 LLM 效率和扩展性的创新。
在过去几个月中,关于开源与商业 AI 之间的复杂关系有很多讨论。短期内,开源社区无法在需要大量数据和/或计算资源的竞赛中跟上。然而,从长远来看,即使是像谷歌和 OpenAI 这样的公司也感受到开源的威胁。[3] 这种紧张关系促使双方继续发展,最终取得的进展逐渐汇聚成富有成果的协同效应。开源社区注重节俭,即通过更少的资源提高 LLM 的效率。这不仅使 LLM 对更广泛的用户群体更具负担能力——即 AI 民主化——而且从环境角度来看也更具可持续性。LLM 变得更高效的主要维度有三个:
-
更少的计算和内存:例如,FlashAttention [4] 允许减少 GPU 上的读写次数,相比于标准的注意力算法,从而实现更快且内存高效的微调。
-
更少的参数:在标准微调中,所有模型权重都会被重新训练——然而,在大多数情况下,只有一小部分权重影响模型在微调数据上的表现。参数高效微调(PEFT)识别这一子集并“冻结”其他权重,这可以大幅减少资源使用,同时实现更稳定的模型表现。
-
更少的训练数据:数据质量比数据规模更具可扩展性[3]——你的训练数据越集中和精心策划,优化性能所需的数据量就越少。其中一种最成功的方法是指令微调。在训练过程中,LLM 会被提供任务特定的指令,这些指令反映了它在推理过程中最终会如何被提示。缩小训练范围可以从更少的数据中更快地学习。指令微调已经被应用了一段时间,例如在 T0、FLAN、InstructGPT 中——最终,它也是 ChatGPT 所基于的方法。
另一方面,目前,“生成性人工智能的控制权掌握在那些能够负担得起训练和大规模部署模型的少数人手中”。[5] 商业产品的规模正在迅速膨胀——无论是模型规模、数据规模还是训练时间——在输出质量方面明显优于开源模型。技术上这里没有太多可以报告的内容——而更多的担忧在于治理和监管。因此,“一个关键风险是像 GPT 这样的强大 LLM 可能只朝着符合这些公司商业目标的方向发展。”[5]
这两端将如何交汇——它们会交汇吗?一方面,任何能够减少资源消耗的技巧最终都可以通过投入更多资源来扩大规模。另一方面,LLM 的训练遵循幂律,这意味着随着模型规模、数据集规模和训练时间的增加,学习曲线会变得平缓。[6] 你可以将其理解为人类教育的类比——在人类历史的长河中,学校教育时间有所增加,但普通人的智力和博学是否也随之增加?
学习曲线平缓的积极之处在于它在对人工智能“变得比人类更强大和聪明”的担忧中带来的缓解。但请做好准备——LLM 领域充满了惊喜,其中最不可预测的之一是涌现。[7] 涌现是指系统中的定量变化导致行为上的定性变化——总结为“量变引起质变”,或者简单地说“更多即不同”。[8] 在训练的某个阶段,LLM 似乎会获得一些新的、意想不到的能力,这些能力不在原始训练范围内。目前,这些能力表现为新的语言技能——例如,模型突然学会了总结或翻译,而不仅仅是生成文本。无法预测何时会发生这种情况,以及这些新能力的性质和范围是什么。因此,虽然涌现现象对研究人员和未来学家来说很吸引人,但在商业背景下仍然远未提供稳健的价值。
随着越来越多的方法被开发出来以提高 LLM 微调和推理的效率,围绕开源 LLM 物理操作的资源瓶颈似乎正在放松。由于商业 LLM 的高使用成本和限制配额,越来越多的公司考虑部署自己的 LLM。然而,开发和维护成本依然存在,大多数描述的优化也需要扩展的技术技能来操作模型及其部署的硬件。选择开源还是商业 LLM 是一个战略性决策,应在仔细探索包括成本(包括开发、运营和使用成本)、可用性、灵活性和性能在内的一系列权衡后做出。一个常见的建议是先使用大型商业 LLM 来快速验证最终产品的商业价值,然后再“切换”到开源 LLM。然而,这种过渡可能会很艰难,甚至不现实,因为 LLM 在擅长的任务上有很大差异。存在着开源模型无法满足你已开发应用需求的风险,或者你需要做大量修改以缓解相关权衡。最后,对于那些在 LLM 上构建各种功能的公司,最先进的设置是多 LLM 架构,这允许利用不同 LLM 的优势。
3. LLM 正在通过插件、代理和框架实现操作。
LLM 训练中的重大挑战已基本解决,另一项工作重点是将 LLM 集成到现实世界的产品中。除了提供提高开发者便利性的现成组件外,这些创新还帮助克服现有 LLM 的限制,并通过推理和使用非语言数据等附加能力来丰富它们。[9] 基本思想是,尽管 LLM 在模拟人类语言能力方面已经很出色,但它们仍需置于更广泛的计算“认知”背景中,以进行更复杂的推理和执行。这种认知包括推理、行动和观察环境等多种不同能力。目前,这通过插件和代理来近似实现,这些插件和代理可以通过 LangChain、LlamaIndex 和 AutoGPT 等模块化 LLM 框架进行组合。
3.1 插件提供对外部数据和功能的访问
预训练的 LLM 在利用数据时存在显著的实际限制:一方面,数据很快就会过时——例如,尽管 GPT-4 于 2023 年发布,其数据却截止于 2021 年。另一方面,大多数现实世界的应用需要对 LLM 中的知识进行一些定制。考虑构建一个允许你创建个性化营销内容的应用——你可以向 LLM 提供关于你的产品和具体用户的信息,结果会更好。插件使这一点成为可能——你的程序可以从外部源(如客户电子邮件和通话记录)获取数据,并将这些数据插入到提示中,以生成个性化的、受控的输出。
3.2 代理人言行一致
语言与可操作性紧密相关。我们的交流意图常常围绕行动展开,例如当我们要求别人做某事或拒绝以某种方式行动时。计算机程序也是如此,它们可以被视为执行特定操作的函数集合,当满足特定条件时会阻止这些操作等。基于 LLM 的代理人将这两个世界结合在一起。这些代理人的指令不是用编程语言硬编码的,而是由 LLM 以推理链的形式自由生成,这些推理链引导实现给定目标。每个代理都有一组插件,并可以根据推理链的需要进行调整——例如,它可以结合一个用于检索特定信息的搜索引擎和一个用于随后对这些信息进行计算的计算器。代理人的概念在强化学习中存在已久——然而,到今天为止,强化学习仍发生在相对封闭和安全的环境中。凭借 LLM 的广泛常识,代理人不仅可以进入“广阔的世界”,还可以发挥无尽的组合潜力:每个代理可以执行多种任务以实现其目标,多个代理可以互相互动和合作。[10] 此外,代理人从与世界的互动中学习,建立的记忆比 LLM 的纯语言记忆更接近人类的多模态记忆。
3.3 框架提供了 LLM 集成的便捷接口
在过去几个月里,我们见证了一系列基于 LLM 的新框架,如 LangChain、AutoGPT 和 LlamaIndex。这些框架允许将插件和代理集成到复杂的生成和操作链中,以实现包括多步骤推理和执行的复杂过程。开发人员现在可以专注于高效的提示工程和快速应用原型设计。[11] 目前,使用这些框架时仍然有很多硬编码的工作——但逐渐地,它们可能会向更全面和灵活的系统发展,比如 Yann LeCun 提出的 JEPA 架构。[12]
这些新组件和框架对构建者有何影响?一方面,它们通过增强外部数据和能力提升了 LLM 的潜力。框架与方便的商业 LLM 结合,将应用原型制作缩短到了几天。但 LLM 框架的兴起也对 LLM 层有影响。它现在隐藏在额外的抽象层之后,任何抽象层都需要更高的意识和纪律,以可持续的方式利用。首先,在生产开发时,仍需要一个结构化的过程来评估和选择适合当前任务的 LLM。目前,许多公司在假设 OpenAI 提供的最新模型最合适的情况下跳过了这个过程。其次,LLM 的选择应该与期望的代理行为协调:期望的行为越复杂和灵活,LLM 应该表现得越好,以确保它能在广泛的选项空间中选择正确的行动。[13] 最后,在操作中,MLOps 管道应该确保模型不会偏离变化的数据分布和用户偏好。
4. LLM 的语言接口为人机交互带来了新的挑战。
随着提示技术的进步,使用 AI 来做炫酷和创造性的事情对于非技术人员变得越来越可行。不再需要成为程序员——只需使用语言,我们的自然沟通媒介,来告诉机器该做什么。然而,在所有围绕快速原型制作和 LLM 实验的热潮和兴奋中,我们仍然会发现“用 LLM 做些炫酷的东西很容易,但要让它们达到生产级的准备却非常困难。”[14] 在生产环境中,LLM 会出现幻觉,对不完美的提示设计很敏感,并且在治理、安全和与期望结果的一致性方面提出了许多问题。我们最喜欢 LLM 的地方——它开放的输入和输出空间——也使得在部署到生产环境之前,更难以测试潜在的故障。
4.1 幻觉和沉默的失败
如果你曾经构建过 AI 产品,你会知道最终用户通常对 AI 故障非常敏感。用户倾向于“负面偏见”:即使你的系统整体准确性很高,那些偶尔但不可避免的错误情况也会被放大镜审视。对于 LLM 来说,情况有所不同。就像任何其他复杂的 AI 系统一样,LLM 也会失败——但它们以沉默的方式失败。即使它们没有一个好的回应,它们仍然会生成某些东西,并以高度自信的方式展示出来,欺骗我们相信和接受它们,并让我们在后续过程中陷入尴尬的境地。想象一下,一个由 LLM 生成指令的多步骤代理——第一步的错误将级联到所有后续任务中,并破坏代理的整个行动序列。
大型语言模型(LLMs)面临的最大质量问题之一是幻觉,指的是生成在语义上或语法上看似合理但事实上却不正确的文本。早在诺姆·乔姆斯基通过他著名的句子“无色的绿色思想愤怒地睡觉”中,就指出了一个句子从语言学角度看可能完美无缺,但对人类而言却完全毫无意义的观点。但对于 LLMs 而言情况不同,它们缺乏人类所拥有的非语言知识,因此无法将语言与基础世界的现实相结合。虽然我们能立即发现乔姆斯基句子中的问题,但一旦进入我们专业领域之外的更专业领域,验证 LLM 输出的准确性会变得相当繁琐。对于长篇内容以及没有真实依据的交互,如预测和开放式的科学或哲学问题,未被发现的幻觉风险尤其高。
对于幻觉有多种处理方法。从统计学的角度来看,我们可以期待随着语言模型的学习越来越多,幻觉现象会减少。但在商业环境中,这种“解决方案”的增量性和不确定的时间线使得它相当不可靠。另一种方法基于神经符号 AI。通过结合统计语言生成和确定性世界知识的力量,我们或许能够减少幻觉和隐性失败,最终使大规模生产中的大型语言模型(LLM)更加稳健。例如,ChatGPT 通过集成 Wolfram Alpha 这一庞大的结构化知识数据库来兑现这一承诺。
4.2 提示的挑战
表面上,提示提供的自然语言界面似乎缩小了 AI 专家和外行之间的差距——毕竟,我们都知道至少一种语言并用它进行沟通,那么为何不在 LLM 中做同样的事?但提示是一门精细的工艺。成功的提示不仅需要强大的语言直觉,还需要对 LLM 的学习和工作原理有深入了解。而且,设计成功的提示过程是高度迭代的,需要系统的实验。如论文 Why Johnny can’t prompt 所示,人类很难保持这种严格性。一方面,我们常常被根植于人际互动经验中的期望所影响。与人交谈不同于与 LLM 交谈——当我们互相交流时,我们的输入是通过丰富的情境背景传递的,这使我们能够中和人类语言中的不精确性和模糊性。LLM 只接收到语言信息,因此宽容度要小得多。另一方面,采用系统化的方法进行提示工程很困难,因此我们很快陷入机会主义的试错中,难以构建一个可扩展且一致的提示系统。
解决这些挑战需要教育提示工程师和用户了解学习过程和 LLM 的失败模式,并保持对界面中可能出现的错误的意识。应该清楚,LLM 的输出总是具有不确定性的。例如,可以通过模型校准得出的置信度分数在用户界面中实现这一点。[15] 对于提示工程,我们目前看到 LLMOps 的兴起,这是 MLOps 的一个子类别,允许通过提示模板、版本控制、优化等管理提示生命周期。最后,在一致性方面,微调胜过少量学习,因为它消除了即兴提示的“人为因素”并丰富了 LLM 的内在知识。只要在你的设置中有可能,你应该考虑在积累了足够的训练数据后从提示切换到微调。
结论
随着新模型、性能优化和集成每天都在出现,LLM 的深度正在不断加深。对公司而言,保持差异化、关注近期发展和新风险以及偏向实际操作实验而非热点是重要的——许多与 LLM 相关的权衡和问题只有在实际使用中才会显现。本文探讨了近期的发展及其对 LLM 构建的影响:
-
大多数当前的 LLM 是自回归的,在生成任务中表现出色。它们在分析任务中可能不可靠,此时应优先考虑自编码 LLM 或其他 NLP 技术。
-
开源 LLM 和商业 LLM 之间存在显著差异,切换 LLM 可能比预期的更困难。仔细考虑权衡,评估可能的发展路径(从开源开始,随后切换到商业 LLM),并考虑如果你产品的不同特性依赖于 LLM 时,是否考虑多 LLM 设置。
-
框架提供了一个便捷的接口来构建 LLM,但不要低估 LLM 层的重要性 — LLM 应经过实验和细致挑选的过程,然后通过完整的 MLOps 周期,以确保稳健、持续优化的操作,并减轻诸如模型漂移等问题。
-
构建者应主动管理人类因素。LLM 已经征服了语言,这是一个最初仅人类能够接触的认知领域。作为人类,我们很快忘记 LLM 仍然是“机器”,并未能如对待机器一样操作它们。对于用户和员工,考虑如何提高他们的意识,并教育他们正确操作和使用 LLM。
参考文献
[1] 安德森·霍洛维茨。2023 年。 对于 B2B 生成 AI 应用程序,少即是多吗?
[2] 李栋等。2019 年。统一语言模型预训练用于自然语言理解和生成。在第 33 届国际神经信息处理系统会议论文集中,第 13063–13075 页。
[3] 信息日报。2023 年。 谷歌研究员:公司在 AI 领域没有“护城河”。
[4] 特里·道等。2022 年。 FlashAttention:具有 IO 感知的快速且内存高效的精确注意力。
[5] EE Times。2023 年。 开源 LLM 能解决 AI 民主化问题吗?
[6] 贾雷德·卡普兰等。2023 年。 神经语言模型的扩展定律。
[7] 杰森·魏等。2023 年。 大型语言模型的涌现能力。
[8] 菲利普·安德森。1972 年。更多即不同。在《科学》,第 177 卷,第 4047 期,第 393–396 页。
[9] 贾娜·利朋科娃。2023 年。 克服大型语言模型的局限性。
[10] 俊尚·朴等。2023 年。 生成代理:人类行为的互动模拟
[11] 哈佛大学。2023 年。GPT-4 — 它是如何工作的,我如何用它构建应用程序? — CS50 技术讲座。
[12] 扬·勒昆。2022 年。 走向自主机器智能的路径。
[13] 杰瑞·刘。2023 年。 更笨的 LLM 代理需要更多的约束和更好的工具。
[14] Chip Huyen. 2023. 构建生产环境中的 LLM 应用。
[15] Stephanie Lin 等. 2022. 教模型用语言表达它们的不确定性。
减少分析浪费的四个步骤
原文:
towardsdatascience.com/four-steps-to-remove-analytics-waste-550f65f9a9e0?source=collection_archive---------13-----------------------#2023-02-03
通过减少分析浪费来加快决策
·
关注 发表在 Towards Data Science ·7 min read·2023 年 2 月 3 日
--
洗车 — 照片由作者 David E Sweenor 提供
思考分析在您的组织决策过程中的作用。您是否认为分析被有效地或高效地使用?或者您是否能够看到宝贵的时间和资源被浪费的方式?在一个经济体中,经理人每年在无效的决策上浪费了超过一百万天的时间,在各个层面上减少浪费的机会是巨大的。
当你怀疑你的组织选择了错误的商业伙伴或者滑向了以前的位置时,你并不孤单。问题可能是你过于依赖共识,你的角色不明确,你的数据过时了,或者——我将在下面描述的——你的文化缺乏授权。在任何情况下,一个组织的决策过程无效的一个重要原因是,他们没有大规模地自动化分析。系统中的浪费导致了缓慢和低效的决策。
决策的组成
接下来,考虑一下你组织做出的不同类型的决策。例如:
-
战略性(我们应该收购一家公司吗?)
-
战术性或交叉性(什么是可接受的保险风险水平?)
-
操作性(我们能向客户提供什么是最好的下一步报价?)
每种决策类型背后都有一系列步骤,大致如下:
业务事件 → 分析准备数据存储 → 分析完成和信息传递 → 做出业务决策 → 采取业务行动
这些步骤之间的联系越紧密,你从竞争格局变化这样的业务事件到市场活动这样的业务行动的过渡速度就越快。但是在你的组织中,这种情况有多常见?难道几乎总会有瓶颈和延迟点以及摩擦,导致从一个步骤到下一个步骤的过渡缓慢吗?
当你在这些步骤的背景下考虑决策时,你会意识到数据的信息价值是易逝的,并且随着时间的推移而衰减。如果你从决策过程中消除分析浪费,你可以变得更加具有竞争力。
四种类型的分析浪费
你组织中的人员不断使用分析工具来生成有关客户流失、商品价格或利率影响的洞察。分析可能是在突出不断变化趋势的先进预测工具中,也可能是在像电子表格这样的传统工具中。
这些洞察会发生什么?在许多情况下,分析完成后,信息被传递了,但之后呢?它可能会被存储在某个地方,几周或几个月后再被重新分析,而那时的行动和业务决策时机已经错过。
这看起来是这样的:
分析过程中的浪费
鉴于数据的信息价值是易逝的,每一个延迟的红色三角形——无论是在数据、分析、决策还是行动周围——都是你竞争力的障碍。这些延迟点加起来就是你在从业务事件到业务行动过程中所面临的延迟。它们代表了你组织中的分析浪费。那么,你能对此做些什么呢?
消除分析浪费的 4 个步骤
当你将决策障碍视为延迟的函数时,你的下一个目标是检查每种延迟类型,并找出导致分析浪费的原因。
-
数据延迟发生在需要访问数据的人无法获得访问权限时。数据访问和共享应该是常态,而不是例外。通过从批处理导向转向更实时的架构来消除分析浪费。例如,那些每天仅监控一次库存水平的零售商在突如其来的暴风雨或假日高峰期可能会面临缺货和损失的风险。
-
分析延迟即使在解决数据延迟后也可能出现。是否因为必须依赖专家来分析数据并反馈你而陷入困境?数据科学家和专家因每年处理少数高价值项目而备受珍视,因此,他们可能需要一段时间才能处理你的任务。在这里,答案在于赋予组织中所有人,无论技能水平如何,参与数据和分析过程的能力。当你的数据科学家专注于几个大问题时,业务中的人员则在数据生成的地方工作,并且可以关注许多小问题。但他们需要数据素养、技能提升和易于使用的软件。
-
决策延迟可能出现,如果决策者被不直观、难以理解的仪表板和报告所困扰。即使人们被授权做决定,他们能否查看眼前的信息并知道需要做出决定?解决这个问题有三个方面:
a. 重新评估流程。是否需要人参与其中?你是否可以简单地通过发出采购订单或发送电子邮件授权来自动化决策?
b. 去中心化决策。授权那些离问题(和数据)最近的人做决定,而无需审批。
c. 在正确的时间将正确的见解提供给正确的决策者。这是一个效率问题;解决方案的一部分在于组织流程,另一部分在于软件。
-
行动延迟指的是一旦做出决策后,组织的敏捷性。你是否能足够迅速地响应以利用有利条件?例如,你可以期望更多地依赖预测分析,但如果你的组织流程陈旧、脆弱且劳动密集,那又有什么意义呢?解决方案是将更多分析自动化引入流程中。这意味着,如果分析显示你的多伦多商店明天将用完米色小部件,那么所有业务流程都应到位以在此之前发货。你能自动化这些流程的程度越高,你就能越早将注意力转向更高价值的任务。
人员的角色
分析自动化是消除分析浪费的核心。矛盾的是,虽然你涉及的人员越多,你越能迅速去除浪费和延迟。原因如下。
如上所述,你的数据科学家的优先任务是高关注度的项目,旨在对你的底线产生可衡量的影响。你孤立的市场预测问题看似紧迫;然而,这并不是全企业范围的问题,所以它将不得不等待数据科学资源。
但如果你的营销分析师或运营专家能在数据上进行分析呢?他们可能不了解数据科学和编程语言,但他们知道业务,他们知道哪些行动是开放的。与其等待数据科学团队的回应,不如直接参与,这样更好。事实证明,你的组织中有很多隐藏在显眼处的公民数据科学家——这是你组织中尚未开发的潜力。
当人们接近业务并被授权做出决策时,他们可以利用易于访问的数据和分析来解决无数的小问题。通过赋能那些:
-
靠近数据
-
对分析倾向较强的人,你可以取得更多进展
-
希望参与去除分析浪费的过程
-
将会接受使用直观的低代码/无代码工具进行技能提升的机会
结论:更有效的决策制定
当你消除延迟并去除分析浪费时,你将捕捉到上述数据中更多的短暂信息价值。随着时间的推移,你的曲线开始呈现以下形式:
将业务事件压缩到业务行动中创造价值
你压缩了步骤序列,并将其向上和向左移动,以实现实时决策和高价值。你的组织在提升业务经理、分析师和掌握数据的公民数据科学家时,可以更快地做出更好的决策。
总结
无效的决策会导致业务结果不佳。
决策无处不在;你的公司每天做出成千上万甚至百万的决策。每年浪费超过 50 万天的时间,可能会使公司每年损失高达 2.5 亿美元的工资[i]。除了工资损失外,糟糕和无效的决策还可能使公司损失 3%的利润,2020 年在美国的金额达到 2700 亿美元[ii]。
这些决策可以是战略性的、运营性的或战术性的。通过将业务事件压缩到业务行动中,你的组织可以创造和捕捉价值——这种价值会被永久性地捕获。
为了消除分析废料,组织可以应用分析自动化技术来压缩业务事件到业务行动的周期。然而,单靠技术无法解决你的问题。解锁组织潜力的关键是提升知识工作者和一线经理的技能。人生中有一些机会,你可以去除某些东西(分析废料),却最终得到更多(价值)。
了解分析自动化如何帮助创建一个更灵活的组织,从而更好地服务客户和客户,同时提升财务成功。阅读我们的电子书,“自动化分析:以人为本的转型商业成果方法。”
该版本首次发表在 Alteryx #INPUT
https://www.alteryx.com/input/blog/four-steps-to-remove-analytics-waste
[i] De Smet, Aaron, Gregor Jost, and Leigh Weiss. 2019. “Three Keys to Better Decision Making | McKinsey.” Www.mckinsey.com. May 1, 2019. www.mckinsey.com/capabilities/people-and-organizational-performance/our-insights/three-keys-to-faster-better-decisions.
[ii] Southekal, Prashanth. 2022. “Council Post: The DEAR Model: A Method to Make Good Business Decisions.” Forbes. March 14, 2022. www.forbes.com/sites/forbestechcouncil/2022/03/14/the-dear-model-a-method-to-make-good-business-decisions/?sh=ae0983d5b3fd.
傅里叶变换在时间序列中的应用:关于图像卷积和 SciPy
傅里叶变换卷积同样适用于图像
·发表在 Towards Data Science ·阅读时间 5 分钟·2023 年 7 月 21 日
--
本文是傅里叶变换在时间序列中的第二篇文章,第一篇文章可以在这里查看:
傅里叶变换在时间序列中的应用:用 numpy 解释快速卷积 [## 傅里叶变换在时间序列中的应用:用 numpy 解释快速卷积
使用傅里叶变换实现的卷积速度提高了 10000 倍
上一篇文章的快速回顾
在上一篇文章中,我解释了傅里叶变换如何非常高效地卷积信号。我展示了使用傅里叶变换的卷积在numpy中比标准代数方法快了多个数量级,并且它对应于一种叫做循环卷积的卷积类型。
在这篇文章中,我想强调循环卷积的含义以及它如何应用于图像。图像也是将一维直观扩展到二维的一个好方法。
所有图像均由作者制作。
使用 scipy 的图像卷积
如果你曾经处理过图像处理中的图像,你很可能遇到过用于应用卷积的函数。图像卷积在各个领域都被广泛应用——图像增强、去噪、分割、特征提取、压缩——并且是卷积神经网络的基础,卷积神经网络是处理视觉数据的深度学习模型的黄金标准。
在Python中,图像卷积可以通过使用scipy及其ndimage子包来简单实现。此时,我建议快速查看一下convolve
函数的文档,然后再回来继续阅读。
[## scipy.ndimage.convolve - SciPy v1.11.1 手册
多维卷积。数组与给定的内核进行卷积。参数:输入数组。数组…
使用方法非常简单:你可以传递两个图像以进行卷积。让我们看一个例子:
注意scipy提供了几种使用参数‘mode’处理边界的方法:如下面所示,模式‘wrap’对应于循环卷积,因此对应于使用傅里叶变换的方法。还存在其他方法,如‘reflect’,它将图像内外翻转,或‘constant’,它重复最外层的值。还要注意‘wrap’的工作方式:它重复整个信号,就好像它是周期性的。
2D 图像的卷积
我们开始编码以查看不同卷积模式之间的差异。
首先,我们创建一个类来表示 2D 周期性图像:记住在上一篇文章中,当使用傅里叶变换工具时,信号被认为是周期性的。这个类只是用于绘制这种 2D 周期数组的语法糖。
我们在[0, V, 0, H]矩形中展示了“基”图像,以及其周围的前 8 个复制品。如前一篇文章所述,信号被视为周期性的,因此具有无限支持,但我们只需要并使用一个周期。
现在我们创建一个样本图像进行测试:它应包含随机噪声、正弦波模式、斜坡模式和几个方形斑点。我们还创建了此样本图像的周期版本:它表示傅里叶变换在应用其算子时所考虑的周期图像。
我们要进行卷积的输入图像,以周期数组表示。中心是“基”图像,其复制品围绕四周。
现在我们创建一个用于卷积的内核:我们将使用一个简单的常量内核,也称为平均内核,因为与此内核的卷积仅给出输入图像的局部平均值。
然后我们开始使用scipy的卷积函数及其不同的modes来处理边界,并将结果包装为周期数组以便于绘图:注意卷积图像的中间部分总是相同的,无论使用何种mode,但边界会有所不同。
使用 scipy 卷积函数处理边界的 4 种不同“模式”的结果。每个结果图像都显示为周期数组。
现在我们可以使用傅里叶变换方法来计算卷积:如前一篇文章所示,我们只需要对两个信号的傅里叶变换的乘积进行逆傅里叶变换,即图像和内核:
计算两个信号的卷积的基本方程,使用傅里叶变换方法。
使用傅里叶变换方法的卷积结果,而不是 scipy。
与 scipy 的“wrap”模式结果进行比较,我们可以看到结果非常相似,只是有一点偏移:
scipy 卷积模式=’wrap’和傅里叶变换方法的比较。它们几乎是相同的。使用周期数组表示,我们可以看到这只是一个偏移的问题。
这只是一个索引的问题,我们可以使用移位中心化内核得到完全相同的结果:
使用 scipy 模式=’wrap’(左侧)的卷积与两种傅里叶变换方法的比较:在两侧填充内核(中间)和中心化内核(右侧)。左侧和右侧的图像是相同的。
使用适当的中心化,我们得到了scipy的卷积与模式=’wrap’以及傅里叶变换方法之间的相同结果。
出于好奇,我们来看看哪种方法更快:
scipy 和傅里叶变换计算两个图像之间卷积的时间比较:傅里叶变换比 scipy 快约 15 倍。
再次,傅里叶变换方法更快,并且在这种情况下比 scipy 函数更快,这很棒。
总结
我们在这篇文章中看到圆形卷积如何转换为图像,以及它如何等同于使用模式=’wrap’的 scipy 卷积函数。
在下一篇文章中,我们将深入探讨在傅里叶变换的背景下使用窗口函数,以减少谱泄漏并改善谱分析。
订阅以直接在您的动态中获取有关傅里叶变换的未来帖子!
另外,请查看我的其他帖子,如果您喜欢其中的任何帖子,请订阅,这对我实现 100 名订阅者的目标帮助很大:
[## 傅里叶变换在时间序列中的应用:使用 numpy 解释的快速卷积
使用傅里叶变换实现的卷积速度快 10000 倍
复习这些著名算法的概念和区别。
## PCA-whitening vs ZCA-whitening: 一个 numpy 2d 可视化
数据的 whitening 过程包括一个变换,使得变换后的数据具有单位矩阵作为…
有限差分法是一种强大的技术,用于解决复杂问题,而 numpy 使其变得更快!
另一阶段的 ipywidgets 和 matplotlib
容器方法。
如果这些文章对你感兴趣,记得关注我,新文章会出现在你的信息流中。
时间序列的傅里叶变换:去趋势
原文:
towardsdatascience.com/fourier-transform-for-time-series-detrending-f0f470f4bf14
对你的时间序列进行去趋势处理可能会改变游戏规则
·发表于Towards Data Science ·8 分钟阅读·2023 年 8 月 11 日
--
在计算傅里叶变换之前对信号进行去趋势处理是一种常见做法,尤其是在处理时间序列时。
在这篇文章中,我想从数学和视觉上展示去趋势处理如何影响信号的傅里叶变换。
所有图像均由作者提供。
这篇文章是我关于时间序列傅里叶变换系列的第四篇:我使用非常简单的示例和少量数学公式来解释傅里叶变换的各种概念。你无需按照下面的顺序阅读,建议在每篇文章之间来回翻阅。
查看之前的帖子:
- 回顾卷积与傅里叶变换的关系及其速度:
10000 倍速度提升的卷积使用傅里叶变换
towardsdatascience.com
- 通过图像示例深入理解卷积:
傅里叶变换卷积也适用于图像
towardsdatascience.com
- 了解如何通过向量视觉方法直观理解傅里叶变换:
绘制傅里叶变换算法以理解它。
towardsdatascience.com
在本帖中,我们将探讨 2 种去趋势:我们将其称为 ‘常数’ 和 ‘线性’ 去趋势。
本帖的最终目标是让你理解什么是常数和线性去趋势,为什么使用它们,以及它们如何影响信号的傅里叶变换。
傅里叶变换快速回顾
在本帖中,我们将使用以下傅里叶变换定义:对于输入序列 x[n],从 n=0 到 N,傅里叶变换的第 k 个系数是以下复数:
本帖使用的傅里叶变换。
常数去趋势
让我们分析输入信号。序列 x[n] 可以分解如下:与其将 x 视为整体,不如将其写作 2 个信号的总和:一个是等于信号均值的“恒定部分”,另一个是给出实际信号与其均值之间差异的“均值周围的变异性”部分:
将 x 分解为 2 部分的总和:一个是常数部分(均值),另一个是“剩余部分”,即均值周围的变异性
因此,对于所有样本 n,我们有:
x 的每个值被分解为 2 部分:均值,加上该值与均值之间的差异。
首先,让我们对 x 的均值进行傅里叶变换:
常数值等于 x 的均值的傅里叶变换:在所有地方都为 0,除了 k=0 处等于输入值。
这是一个简单的序列,在采样 k=0 时其值为 x 的均值,其余地方为 0。使用之前帖子的代码,我们可以轻松理解为什么以下是正确的:
指数的总和为 0,除了 k=0。这可以在下图中图示。
import numpy as np
import matplotlib.pyplot as plt
N = 10
ns = np.arange(N)
fig, axes = plt.subplots(1, N//2+1, figsize=(18,8), sharex=True, sharey=True)
for k in range(0, N//2+1):
eiks = np.exp(-2*1J*np.pi*ns/N*k)
pretty_ax(axes[k])
plot_sum_vector(eiks, axes[k])
axes[k].set_title(f'k={k}')
axes[k].set_aspect('equal')
fig.suptitle(f'Complex plot of the $e^{{-2i\pi kn/N}}$ families')
绘制所有指数的总和可以显示为什么它们总是加起来为 0(除了 k=0)。
现在让我们对 x 进行傅里叶变换,如我们所写的,有其 2 个部分:
对分解后的 x 进行傅里叶变换,得到 2 个傅里叶变换的总和:一个是“变异性”部分的傅里叶变换,另一个是 k=0 时等于均值的系数。
换句话说,x 的傅里叶变换是其均值周围的傅里叶变换的总和,加上一个在所有地方都为 0 但在 k=0 时等于 x 均值的序列。
这就是常数去趋势的含义:即在进行傅里叶变换之前去除信号的均值。在傅里叶系数方面,这相当于将 k=0 的系数设置为 0。
另一种理解方式如下:可以很容易地证明,当 k=0 时的系数总是等于信号的均值:
证明 0-th 系数等于 x 的均值。
线性去趋势
方法与之前相同:将输入信号写成 2 部分的和:一个“线性”部分,以及围绕该线性部分的其他变异:
将 x 分解为 2 部分:一个“线性部分”和“其余部分”。
其中线性部分通常是通过最小二乘拟合计算出来的。使用索引,我们可以将线性部分写为:
线性部分的表达。
其中 b 是信号的均值。
现在我们已经写出了 x 的分解,接下来进行傅里叶变换:
将 x 的傅里叶变换分解为 2 个傅里叶变换的和。
线性部分的傅里叶变换,由于傅里叶变换的线性特性:
线性部分的傅里叶变换分解。
因此,线性去趋势的过程就是在进行傅里叶变换之前去除 x 的线性部分:这会从结果中去除项 aFT(n)+b,其中 a 是一个常数因子(对应于线性拟合的斜率),FT(n)是线性序列[0, 1, …]的傅里叶变换,而 b 是信号的均值(因此第一个傅里叶系数将为 0,就像常数去趋势一样)。
在 python 中进行去趋势
让我们看看如何在 python 中简单地去趋势信号并进行傅里叶变换。使用 numpy 和 scipy 来实现非常直接。
Scipy 在其signal包中提供了一个detrend函数,并且有一个type参数来指定是否要对信号进行常数去趋势或线性去趋势。
在下面的示例中,我们创建了一个长度为 20 样本的信号,其中包含一个主系数为 2 的线性部分、一点噪声、一个 4 的偏移量和一个正弦部分。
import numpy as np
from scipy.signal import detrend
import matplotlib.pyplot as plt
N = 20
# create a sample signal, with linear, offset, noise and sinus parts
ys = np.arange(N) * 2 + 4 + np.random.randn(N) + 4*np.sin(2*np.pi*np.arange(N)/5)
# constant and linear detrend
ys_c = detrend(ys, type='constant')
ys_l = detrend(ys, type='linear')
fig, axes = plt.subplots(1, 2)
ax = axes[0]
ax.plot(ys, label='raw')
ax.plot(ys_c, label='constant-detrended')
ax.plot(ys_l, label='linear-detrended')
ax.legend()
ax.set_title('Input signal')
ax = axes[1]
# we use rfft since our input signals are real
ax.plot(np.abs(np.fft.rfft(ys)))
ax.plot(np.abs(np.fft.rfft(ys_c)))
ax.plot(np.abs(np.fft.rfft(ys_l)))
ax.set_title('Module of Fourier-transform')
左侧:输入信号、常数去趋势和线性去趋势。右侧:它们的傅里叶变换的幅度。
让我们回顾这些图表。
左侧是原始输入信号,以及它的常数去趋势和线性去趋势版本。
常数去趋势有效地去除了信号的均值,使其中心围绕 0。线性去趋势不仅去除信号的均值,还去除其线性趋势(即“直线斜率”)。在视觉上,在线性去趋势信号上比在原始信号上更容易识别出正弦部分。
右侧是每个信号的傅里叶变换的模:如果没有进行去趋势处理,我们会得到蓝色的模。通过常量去趋势处理去除均值实际上将 0 阶系数设为 0,这通常使得图形更容易分析。但最好的部分来自线性去趋势处理:正如你所看到的,输出的傅里叶系数很好地显示了输出谱中的正弦频率。
所以,假设你正在分析时间序列并使用傅里叶谱查找季节性模式:如果你的信号已经进行了线性去趋势处理,这将更容易。
更进一步,线性去趋势的主要优点是它大大减少了谱泄漏。 我们将在另一篇文章中详细讨论什么是谱泄漏以及为什么我们要去除它。
关于线性信号的傅里叶变换
我们可以很容易地绘制线性信号 Kn 的傅里叶变换,其中 K 是斜率,针对不同的 K 值:
import numpy as np
import matplotlib.pyplot as plt
N = 10
ns = np.arange(N)
Ks = [-5, 2, 5]
fig, axes = plt.subplots(len(Ks), N//2+1, figsize=(18,8), sharex=True, sharey=True, gridspec_kw={'hspace':0, 'wspace':0})
for i, K in enumerate(Ks):
xs = K*np.arange(N)
for k in range(0, N//2+1):
Zs = xs * np.exp(-2*1J*np.pi*ns/N*k) / N
ax = axes[i, k]
pretty_ax(ax)
plot_sum_vector(Zs, ax)
ax.set_aspect('equal')
ax.set_xlabel(f'k={k}')
axes[i, 0].set_ylabel(f'K={K}')
fig.tight_layout()
线性信号的傅里叶系数 K*n。第一行:K=-5。第二行:K=2。第三行:K=5。
正如你所见,对于给定的 k 值,傅里叶系数(由红色箭头表示)总是对齐的,并且相等,只是尺度不同。因此,输出谱中去除的部分始终是序列 [0, 1, …N] 的傅里叶变换,其缩放因子由线性拟合的斜率给出。
总结
在这篇文章中,我们了解了常量去趋势和线性去趋势的概念:它们分别是去除输入信号的均值或线性拟合。这一步骤在计算傅里叶变换之前有助于使输出谱更容易解释。
去除信号的均值将 0 阶系数设为 0。结果图形更易于检查,因为大多数情况下,均值相对于谱的其他部分可能非常大。因此,如果去除该系数,y 轴的尺度更容易设置。
去除线性部分,除了去除均值外,还去除了信号中的整体趋势,这通常是原始信号中主导的部分,并且可能掩盖了你真正感兴趣的其他组件/季节性行为。
如果你喜欢这篇文章并想阅读更多内容,请订阅 😃 !
每当我发布时获取电子邮件!新的出版物将包括数据转换、先进的绘图和模拟……
确保查看我其他与傅里叶变换无关的文章:
- 使用 numpy 的有限差分法:
有限差分法是一种强大的技术来解决复杂问题,而 numpy 使其更快!
towardsdatascience.com
- PCA、ICA 和 LDA 算法的比较:
回顾这些著名算法的概念和差异。
towardsdatascience.com
- 比较 PCA 白化和 ZCA 白化:
## PCA 白化 vs ZCA 白化:一个 numpy 2d 可视化
数据白化的过程包括一种变换,使得变换后的数据具有单位矩阵作为…
towardsdatascience.com
时间序列的傅里叶变换:使用 numpy 解释快速卷积
从头实现与 numpy
·发布于 Towards Data Science ·7 分钟阅读·2023 年 7 月 3 日
--
傅里叶变换算法被认为是所有数学领域的伟大发现之一。法国数学家让-巴蒂斯特·约瑟夫·傅里叶在 1822 年的著作《热的解析理论》中奠定了谐波分析的基础。今天,傅里叶变换及其所有变种构成了我们现代世界的基础,支持着压缩、通信、图像处理等技术。
法国数学家让·巴蒂斯特·约瑟夫·傅里叶(1768–1830)的雕刻肖像,19 世纪早期。[来源:wikipedia,图片来自公有领域]
这个出色的框架还提供了分析时间序列的优秀工具……这就是我们在这里的原因!
本文是傅里叶变换系列的一部分。今天我们将讨论卷积以及傅里叶变换如何提供最快的实现方法。
所有图形和方程均由作者制作。
离散傅里叶变换(DFT)的定义
让我们从基本定义开始。离散时间序列 x 的离散傅里叶变换是:
离散傅里叶变换(DFT)的定义。还有其他定义,你只需选择一种并坚持(由作者提供)
其中 k 表示 x 的频谱的第 k 个频率。注意,一些作者在该定义中添加了 1/N 的缩放因子,但对于这篇文章来说并不重要——总的来说,这只是定义的问题,并且要坚持定义。
然后逆傅里叶变换是(给定傅里叶变换的定义):
离散傅里叶变换,基于上述正向定义(作者制作)。
也就是说,傅里叶变换中最重要的定理之一是空间中的卷积等同于另一空间中的乘法。换句话说,乘积的傅里叶变换是各自傅里叶谱的卷积,而卷积的傅里叶变换是各自傅里叶谱的乘积。
时间域中的乘法对应于傅里叶域中的循环卷积(作者制作)。
和
时间域的循环卷积对应于傅里叶域的乘法(作者制作)。
其中点表示标准乘法(乘法),而圈中的星号表示循环卷积。
两个重要的注意事项:
-
周期信号:傅里叶分析框架认为我们处理的信号是周期性的。换句话说,它们从负无穷到正无穷重复。然而,用有限内存的计算机处理这种信号并不总是实际的,因此我们只“处理”一个周期,正如我们接下来将看到的那样。
-
循环卷积:卷积定理指出乘法等同于循环卷积,这与我们更熟悉的线性卷积稍有不同。正如我们将看到的,它并没有那么不同,也不那么复杂。
循环卷积与线性卷积
如果你对线性卷积(通常简单称为‘卷积’)比较熟悉,你不会对循环卷积感到困惑。基本上,循环卷积只是处理周期性信号的一种方式。正如你可以猜测的,线性卷积仅对有限长度的信号有意义,这些信号从负无穷到正无穷延展。在我们的案例中,在傅里叶分析的背景下,我们的信号是周期性的,因此不满足这个条件。我们不能讨论(线性)卷积。
然而,我们仍然可以直观地感受到周期信号上的线性卷积类似操作:只需在一个周期长度上卷积周期信号。这就是循环卷积的作用:它在一个周期跨度内卷积两个相同长度的周期信号。
为了进一步确认这些差异,请比较离散线性卷积和离散循环卷积的公式:
线性卷积的公式:在信号处理过程中,大多数时候使用这个公式,通过零填充(作者制作)。
循环卷积:这是在处理周期性信号时使用的卷积,如在傅里叶分析中(作者制作)。
注意差异:
-
边界:线性卷积使用从负无穷到正无穷的样本——如前所述,在这种情况下,x 和 y 具有有限的能量,因此求和是有意义的。对于循环卷积,我们只需要在一个周期范围内发生的情况,因此求和仅跨越一个周期。
-
循环索引:在循环卷积中,我们使用模运算“包裹”y 的索引,模的长度为 N。这只是确保 y 被视为周期为 N 的周期性序列的一种方法:当我们想知道位置 k 处 y 的值时,我们只需使用位置 k%N 处 y 的值——由于 y 是 N 周期性的,我们得到正确的值。同样,这只是处理周期性无限长度样本序列的数学方法。
numpy 中的实现
Numpy 为有限长度信号提供了很好的工具:这真是个好消息,因为正如我们刚才看到的,我们的无限长度周期信号只需用一个周期表示。
让我们创建一个简单的类来表示这些信号。我们添加一个方法来快速绘制数组,并在“基本”数组的前后添加额外的周期,以便记住我们处理的是周期序列。
让我们看两个例子:首先是采样的正弦序列,然后是线性序列。两者都被认为是 N 周期性(此例中 N=10)。
PeriodicArray 的两个例子:“基本”周期从 0 到 N 用深蓝色绘制,同时在前后添加了 2 个其他周期,以表示我们正在处理周期序列(作者制作)。
缓慢方式的循环卷积
现在让我们实现上述循环卷积方程。使用索引和模运算符,这非常简单:
上述两个周期序列之间的循环卷积(作者制作)。
太棒了,我们现在可以看到两个信号之间的循环卷积是什么样的。将所有内容放在一个图中:
左侧:第一个周期性数组。中间:第二个周期性数组。右侧:两个周期性数组的循环卷积,它也是一个周期性数组(作者制作)。
现在这个解决方案工作得很好,但它有一个主要缺陷:它很慢。如您所见,我们必须通过两个嵌套的循环来计算结果:一个用于结果数组中的每个位置,另一个用于计算该位置的结果:我们称该算法为 O(N²),随着 N 的增加,操作数量将增加到 N 的平方。
对于像示例中的这些小数组,这不是问题,但随着数组的增长,它将成为一个主要问题。
此外,对数值数据进行循环通常被认为是 Python 中的不良实践。一定有更好的方法……
傅里叶方式的循环卷积
这就是傅里叶变换和卷积定理发挥作用的地方。由于离散傅里叶变换以非常快速和优化的方式实现(使用快速傅里叶变换 FFT),操作是非常 快的(我们说 FFT 是 O(N log N),这比 O(N²) 好得多)。
利用卷积定理,我们可以使用 2 个序列的离散傅里叶变换(DFT)的乘积,当通过逆 DFT 转换回时间域时,我们得到输入时间序列的卷积。换句话说,我们有:
使用直接和逆傅里叶变换进行 x 和 y 的循环卷积(由作者制作)。
其中 DFT 代表离散傅里叶变换,IDFT 代表逆变换操作。
我们可以实现这个算法,使用 numpy 很容易地计算 x 和 y 的卷积:
数值和时间比较
最后,让我们验证这两种方法是否得到相同的结果,并比较 Python 计算循环卷积所需的时间:
比较计算两个周期序列之间的循环卷积的两种方法: “慢方法” 是使用蓝色的循环和加法进行简单代数运算,叠加在橙色的“傅里叶方法”上。这两种方法给出的结果完全相同(数值精度一致)(由作者制作)。
这是一个完美的匹配!两者在数值值上严格等价。
现在进行时间比较:
结果是:
-
对于 N=10 个样本,DFT 快了 6 倍
-
对于 N=1000 个样本,DFT 快了大约 10000 倍
这真是太棒了!现在考虑一下,当你用数以千计的样本分析时间序列时,这将为你带来什么!
总结
我们在这篇文章中看到,傅里叶变换是一个强大的工具,特别是得益于卷积定理,它允许我们以非常高效的方式计算卷积。我们看到线性和循环卷积并不是完全相同的操作,但都是基于卷积的。
订阅以便直接在你的信息流中获取关于傅里叶变换的未来文章!
另外,查看我其他的文章,如果你喜欢其中的任何一篇,请订阅,这对我达到 100 名订阅者的目标帮助很大:
使用 NumPy 实现有限差分法的 300 倍加速 | Yoann Mocquin | Towards Data Science (medium.com)
PCA/LDA/ICA : 组件分析算法比较 | Yoann Mocquin | Towards Data Science (medium.com)
包装 numpy 数组。容器方法。 | Yoann Mocquin | Towards Data Science (medium.com)
深入探讨 Seaborn:颜色调色板 | 作者:Yoann Mocquin | Analytics Vidhya | Medium
时间序列的傅里叶变换:绘制复数
原文:
towardsdatascience.com/fourier-transform-for-time-series-plotting-complex-numbers-9743ffe8a8bb
绘制傅里叶变换算法以理解它
·发表于 Towards Data Science ·12 分钟阅读·2023 年 7 月 28 日
--
大多数时候,人们在处理信号的傅里叶变换时会遇到困难,因为其形式复杂。除非在非常特定的情况下,时间序列的傅里叶变换通常是复数序列——而复数并不总是容易掌握,尤其是当你不习惯处理这些数字时。
在本文中,我想展示几种可视化 1D 实数序列的傅里叶变换的方法,这正是你 99%的时间处理的内容,特别是在数据分析和时间序列中。
所有图像均由作者提供。
本文是我的《时间序列的傅里叶变换》系列的第三篇。查看之前的文章请访问这里:
- 回顾卷积如何与傅里叶变换相关以及其速度:
使用傅里叶变换进行 10000 倍更快的卷积
towardsdatascience.com
- 通过图像示例加深对卷积的理解:
傅里叶变换卷积也适用于图像
towardsdatascience.com
在深入实际计算和绘制一维傅里叶变换之前,我们将回顾一些对接下来的内容至关重要的复杂数基本概念。正如你将看到的,复杂数实际上非常简单:可以将它们视为两个数字的向量。
本文的最终目标是让你对傅里叶变换背后的实际数字更加熟悉。
复杂数的快速回顾
任何复杂数都可以用其标准形式表示,使用两个实值 a 和 b,分别称为其“实部”和“虚部”:
其中 i 是单位复数,具有著名的属性,即其平方等于 -1:
我们还可以将上述方程重新写作如下:
或者再次使用向量的点积:
如果我们在 (x, y) 平面上表示 Z,x 轴为实部 (1),y 轴为虚部 (i),我们将 Z 绘制为从 (0, 0) 到 (a, b) 的向量。
让我们使用 NumPy 和 Matplotlib 来绘制一个示例。
%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
# We start by defininf helper functions
# one to pretify an axes
def pretty_ax(ax, b: float=8):
"""Prettify an Axes for plotting complex numbers:
- Add labels
- Set aspect to 'equal'
- Add grid
- Add unit circle with canonical vectors
- Set custom value for boundaries of axes
"""
ax.set_aspect('equal')
ax.set_xlabel("Real", color="gray")
ax.set_ylabel("Imag", color="gray")
ax.xaxis.set_label_coords(0.5,0.05)
ax.yaxis.set_label_coords(0.05,0.5)
ax.grid(ls='--')
c = plt.Circle((0,0), 1, ls='--', facecolor=None, edgecolor="gray", fill=False)
ax.add_patch(c)
ax.set_xlim(-b, b)
ax.set_ylim(-b, b)
plot_vector(1, ax, color="gray")
plot_vector(1j, ax, color="gray")
# one to plot complex numbers to a 2D ax
def plot_vector(
Z, ax,
color=None, decomp=False, txt=False,
orig=0+0J, ls=None, alpha=1,
arrowstyle=None, width=None,
headwidth=None, frac=None
):
"""Plot a complex number as vector.
"""
x = Z.real
y = Z.imag
arrow_props = dict(
color=color,
ls=ls,
edgecolor=None,
alpha=alpha,
)
if arrowstyle is None:
if width is not None:
arrow_props["width"] = width # width of the arrow in points
if headwidth is not None:
arrow_props["headwidth"] = headwidth # width of the base of the arrow head in points
arrow_props['shrink'] = 0.0 # move the tip and base some percent away from the annotated point and text
if frac is not None:
arrow_props['frac'] = frac # fraction of the arrow length occupied by the head
else:
arrow_props['arrowstyle'] = arrowstyle
ann = ax.annotate("",
xy=((Z+orig).real, (Z+orig).imag),
xytext=(orig.real, orig.imag),
arrowprops=arrow_props,
annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
if decomp:
ann = ax.annotate('', xy=(x, 0), xytext=(orig.real, orig.imag), arrowprops=dict(arrowstyle="->", color=color), annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
ann = ax.annotate('', xy=(x, y), xytext=(x,0), arrowprops=dict(arrowstyle="->", color=color), annotation_clip=False)
ann.arrow_patch.set_clip_box(ax.bbox)
if txt:
ax.text(x/2, y/2, f'Z={Z}', ha='center', va='bottom', rotation=np.angle(Z)*180/np.pi, color=color)
fig, ax = plt.subplots()
# prettify the ax for plotting complex numbers
pretty_ax(ax)
# define 2 complex numbers
Z1 = 5 + 3J
Z2 = 1 + 6J
plot_vector(Z1, ax, color="red", txt=True) # plot Z1
plot_vector(Z2, ax, color="blue", txt=True) # plot Z2
plot_vector(Z2, ax, orig=Z1, color="blue") # plot Z2 at the end of Z1
plot_vector(Z1+Z2, ax, txt=True, color="green") # plot Z1+Z2
绘制两个复杂数作为向量(红色和蓝色),以及它们的和(绿色)。
如你所见,表示复杂数 Z1 和 Z2 在二维平面上作为向量是很简单的。因此,我们可以轻松地计算和绘制两个复杂数的和:我们只需将向量相加。这就是所有需要记住的内容;接下来的一切只是花哨的符号和绘图,以及一些有用的数学性质。
另一种表示相同向量的方法是使用极坐标表示法:
其中 ∣Z∣ 被称为 Z 的“模”,θ 被称为“幅角”(我通常称这个变量为“相位角”)。在几何上,模表示向量的长度,而其相位角表示向量相对于 Ox 轴的角度,也称为方向。
标准表示法 (a, b) 与极坐标表示法(模,幅角)之间存在直接对应关系;这只是另一种书写相同向量的方式。确实,请记住,复杂指数就是一个余弦加上一个虚数正弦的和:
因此,如果我们绘制这个复杂数,实部是 cos(θ),虚部是 sin(θ);在复杂平面上的坐标是 (cos(θ),sin(θ))。将这个概念扩展到任何复杂数,我们得到实部 ∣Z∣cos(θ) 和虚部 ∣Z∣sin(θ)。
使用 NumPy,我们可以轻松计算复杂数的实部、虚部、模和相位分量。我们甚至可以检查两种表示法是否得到完全相同的复杂数。确实:
Z = 1+3J
real = Z.real
imag = Z.imag
mod = np.abs(Z)
angle = np.angle(Z)
print(Z.real, Z.imag, mod, angle)
# check that both representation are equivalent
print(real+imag*1J == mod * np.exp(1J*angle))
1.0 3.0 3.1622776601683795 1.2490457723982544
True
现在我们知道复数的行为就像向量一样,我们可以利用关于向量的知识来应用于复数。例如,当我们加两个复数时,它们的和只是对应于底层向量和的复数。让我们再次使用 Python 来查看一个示例:
fig, axes = plt.subplots(1, 2, sharex=True, sharey=True)
pretty_ax(axes[0])
pretty_ax(axes[1])
# define 2 complex numbers
Z1 = 2.5 + 2J
Z2 = 0.5 + 1.5J
# compute their sum
Z_sum = Z1 + Z2
plot_vector(Z1, axes[0], color="red") # plot Z1 alone
plot_vector(Z2, axes[0], orig=Z1, color="blue") # plot Z2 after Z1
plot_vector(Z_sum, axes[0], color="green") # plot their sum
axes[0].set_title('Sum of 2 complex numbers')
# same for the mean
Z_mean = np.mean([Z1, Z2])
plot_vector(Z1, axes[1], color="red") # plot Z1 alone
plot_vector(Z2, axes[1], color="blue") # plot Z2 alone
plot_vector(Z_mean, axes[1], color="green")# plot their mean
axes[1].set_title('Mean of 2 complex numbers')
2 个复数的和(左)与均值(右)的图示。
同样,复数的均值就是向量的和,沿其方向缩放向量的数量。缩放向量对应于增减其长度,但不改变其方向。对于缩放因子 K:
这是一个新的复数,具有新的长度 ∣Z′∣=K∣Z∣(即我们缩放了模),但相位角相同(即向量具有相同的方向)。
所以请记住:
-
相同的复数 Z 可以表示为 Z=a+ib 或 Z=∣Z∣e^(−iθ),其中 ∣Z∣ 代表向量的长度,θ 代表其方向。
-
对复数进行求和/平均的行为与对向量进行求和/平均的行为完全一样。
傅里叶变换的向量方法
让我们在离散样本序列的背景下回顾傅里叶变换的公式,这称为离散傅里叶变换(DFT):
其中
请注意,存在使用无缩放系数、1/N 或 1/N**0.5 的几种定义。我通常更喜欢使用无缩放因子,但在此帖中,我将使用上述方程中的 1/N 缩放因子。
让我们描述一下这个方程告诉我们的内容:
傅里叶变换的第 k 个元素(X[k])是一个复数,由一组复数(Zkns)的均值给出。
利用我们现在对复数和向量的理解,我们可以通过绘制所有的小复数 Zkn,将它们相加(像常规的向量和一样),并将结果缩小 1/N 来获得傅里叶变换的第 k 个系数。
让我们使用 Python 绘制一个示例:
from matplotlib.cm import viridis
N = 10
colors = viridis(np.linspace(0, 1, N))
x_n = np.sin(np.linspace(0, 3, N)) +0.2
ns = np.arange(0, N)
k = 1
Z_ks = x_n * np.exp(-2*1J*np.pi*k*ns/N)
fig, axes = plt.subplots(1, 2)
axes[0].scatter(ns, x_n, marker='o', c=colors)
axes[0].set_title('Input sequence, \neach sample has a specific color')
for i, x in enumerate(x_n):
_Zx = 1J * x
plot_vector(_Zx, axes[0], arrowstyle="->", ls='--', color=colors[i], orig=i)
pretty_ax(axes[1], b=2)
def plot_sum_vector(Z_ks, ax, colors=colors):
cumsum = 0
for Zk, color in zip(Z_ks, colors):
plot_vector(Zk, ax=ax, color=color, arrowstyle='->', ls='--')
plot_vector(Zk, orig=cumsum, ax=ax, color=color)
cumsum += Zk
plot_vector(cumsum, ax=ax, color="red", alpha=0.5)
plot_sum_vector(Z_ks, axes[1])
axes[1].set_title('Fourier coefficient X[k=1]')
fig.tight_layout()
我们想要计算傅里叶变换的输入序列(左)和傅里叶系数 X[k=1] 的视觉计算(右)。
左侧是我们想要计算傅里叶变换系数的输入序列,X[k] 对于 k=0 到 N−1。为了更好地理解发生了什么,每个样本都使用了颜色。
右侧显示了所有的向量 Zkn,每个向量都有一个对应的颜色。注意向量 Zkn 的长度由 x[n] 给出。这些向量被绘制了两次:一次从 (0,0) 开始,另一次则是累积地加和。总和对应于黄色箭头的末端,这个末端等于红色向量。这个红色向量表示傅里叶变换系数 X[k=1]。
这样,我们得到了表示傅里叶变换第 k 个系数的向量:换句话说,我们刚刚计算了 X[k] 的值。我们可以使用相同的步骤计算每个系数,循环遍历 k。
在下面的示例中,我们计算了所有 k=0 到 N−1 的小向量系列。对于每个 k 值,我们绘制了所有小向量 Zks,累积向量和,以及最终的总和(用红色表示)。这样,最终的红色箭头表示该 k 值的傅里叶变换值:它只是一个具有特定模和相位角的复数。换句话说,任何 k 的红色箭头只是 X[k] 的向量表示。
最后,我们检查了傅里叶系数的“手动”计算是否与 numpy 的结果一致。
fig, axes = plt.subplots(2, 5, figsize=(17,9), sharex=True, sharey=True)
#fig.subplots_adjust(top=0.95, bottom=0.3, left=0.05, right=0.95)
#ax_X = fig.add_axes([0.17, 0.05, 0.7, 0.2])
ks = np.arange(N)
X_ks = []
for k, ax in zip(ks, axes.flat):
Z_ks = x_n * np.exp(-2*1J*np.pi*k*ns/N)
pretty_ax(ax, b=6)
plot_sum_vector(Z_ks, ax)
ax.set_title(f'k={k}')
# store the average all of the Zks for that value of k : this IS the fourier
# coefficient for index k
X_ks.append(Z_ks.mean())
X_ks = np.array(X_ks)
fig.tight_layout()
# Finaly, we can check that the Fourier-coefficients we computed are identical
# to those computed by numpy. We use norm='forward' so numpy uses the 1/N scale
# convention.
assert np.allclose(X_ks, np.fft.fft(x_n, norm='forward'))
所有傅里叶系数的视觉计算表示,对于 k=0 到 N-1。最终 X[k] 的值表示为该 k 值所有彩色向量的总和。
就这样:傅里叶系数 X[k] 由红色箭头表示,对于每个 k 值。
几点需要注意:
-
对于 k=0,所有向量都在 x 轴上对齐,因此它们正好加在一起,作为实值的和。
-
对于 k=1 和 k=9,彩色向量整齐地加在一起,得到一个复数和向量。
-
对于其他 k 值,小向量基本上会相互抵消。
现在我们知道如何直观地计算傅里叶变换的每个元素,最终结果是整个离散傅里叶变换序列,这是另一系列复数。在下一部分,我们将绘制整个复数序列。
绘制傅里叶变换序列
现在我们知道傅里叶变换系数 X[k] 如何通过数学和视觉方法计算,结果是复数序列 X[k],我们可以用多种方式绘制:
-
绘制实部和虚部。
-
绘制模和相位角。
-
绘制 2D 向量,沿第三轴表示索引 kk。
使用上面看到的 numpy 函数,我们可以轻松提取所有这些特征并绘制它们。
fig = plt.figure()
ax = fig.add_subplot(121, projection='3d', proj_type = 'ortho')
Zs = ks
Xs = X_ks.real
Ys = X_ks.imag
ax.plot(Xs, Ys, Zs, "--", color='red')
ax.scatter(Xs, Ys, Zs, '-o', color='red')#colors)
ax.set_xlabel('Real')
ax.set_ylabel('Imag')
ax.set_zlabel('k')
ax.set_title("Fourier coefficients X[k]")
ax.quiver(np.zeros_like(Xs), np.zeros_like(Xs), ks,
Xs, Ys, np.zeros_like(Xs),
colors='red',#colors,
arrow_length_ratio = 0.3, lw=3)
zmin, zmax = ax.get_zlim()
ax.plot([0,0], [0,0], [zmin, zmax], color="r", alpha=0.5, ls="--")
ax = fig.add_subplot(322)
ax.plot(x_n, '-o')
ax.set_title("Input sequence")
ax = fig.add_subplot(324)
ax.plot(ks, X_ks.real, '--o', color="r", label="real")
ax.plot(ks, X_ks.imag, '--o', color="g", label='imag')
ax.set_title('Real and imaginary parts')
ax.set_xlabel("k")
ax.legend()
ax = fig.add_subplot(326)
ax.plot(ks, np.abs(X_ks), '--o', color="r", label="mod")
ax.plot(ks, np.angle(X_ks), '--o', color="g", label='angle')
ax.set_title('Module and phase angle')
ax.set_xlabel("k")
ax.legend()
fig.tight_layout()
输入序列(右上角),以及表示其傅里叶变换的三种方式。
-
首先是 3D 图:z 轴表示傅里叶变换序列中 X[k] 的每个索引 k。在每个 z=k 的 XY 平面上,绘制了 X[k] 向量,实部在 x 轴上,虚部在 y 轴上。也就是说,请注意 X[k] 系数几乎完全是实数,向量几乎与 y 轴对齐,表示虚部非常小。这是因为我们的输入序列几乎完全对称,因此其傅里叶变换几乎完全是实数(这是我们将来会深入探讨的一个属性)。
-
其次是实部和虚部图:另一种可视化傅里叶变换序列的方式是绘制其实部和虚部。这与对齐 3D 图视图到 x 轴或 y 轴时看到的值基本相同。虽然这种方式在直观理解和与 3D 图比较时更容易,但使用的频率不高。
-
最后是模长和相位角图:记住,模长实际上是每个向量的长度,而相位角表示它们的方向。从 3D 图中更容易理解模长,因为它简单地给出向量的长度,而在实部/虚部图中不那么明显。关于相位角,
总结
这里是你绝对应该记住的关键点:
-
复数只是向量,实部在 x 轴上,虚部在 y 轴上。
-
它们像向量一样表现:你知道向量的加法和缩放意味着什么,因此你也知道它们在复数上的作用。
-
你可以使用复数的各种表示/分解方式:作为 2D 向量,或提取其实部/虚部,其长度及其模,以及其方向和相位角。
-
傅里叶变换是一个复数序列:每个复数本身是其他复数序列的和(或平均值),这些复数具有模长(即长度)x[n]x[n] 和相位角(即方向)−2πkn/N。
with
如果你喜欢这篇文章,查看我的其他文章:我通常尝试使用简单的 numpy 和 matplotlib 代码来解释概念:
介绍其中一个最著名的统计检验
mocquin.medium.com](https://mocquin.medium.com/one-sample-t-test-visually-explained-415c31744e14?source=post_page-----9743ffe8a8bb--------------------------------) ## PCA/LDA/ICA:组件分析算法比较
复习这些著名算法的概念和差异。
towardsdatascience.com [## PCA 白化与 ZCA 白化:一个 numpy 2D 可视化
数据白化过程包括一个变换,使得变换后的数据具有单位矩阵作为…
有限差分法是一种强大的技术,用于解决复杂问题,而 numpy 使得这一过程变得快速!
深入探讨 seaborn 调色板
沉浸在 seaborn 调色板中吗?
进一步探索 ipywidgets 和 matplotlib
傅里叶变换,实用的 Python 实现
真实世界信号的实际应用
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 2 月 27 日
--
傅里叶变换是信号处理和时间序列分析中最著名的工具之一。快速傅里叶变换(FFT)是傅里叶变换在数字信号上的实际应用。FFT 被认为是 20 世纪对科学和工程影响最大的前 10 个算法之一 [1]。
图片由 Edz Norton 提供,来源于 Unsplash
在这篇文章中,讨论了 FFT 的实际应用,即如何使用它来表示信号数据的频率域(谱),并使用 Plotly 绘制谱图,以提供更多互动性,并更好地理解谱中的特征。到文章末尾,我们将构建一个类来分析信号。提供了一个 ECG 信号及其谱作为最终示例。
使用我们类 Fourier 的表示 [作者提供的图片]
介绍
傅里叶变换(FT)将信号的时间域与其频率域联系起来,其中频率域包含构成信号的正弦波(幅度、频率、相位)的信息。由于 FT 是一个连续变换,离散傅里叶变换(DFT)在数字世界中成为适用的变换,它以离散格式的样本集合形式保存信号信息,其中采样定理是离散化和信号的严格规则。具有 N 个样本的信号(xn)的 DFT 由以下方程给出 [2]:
DFT 方程 [2]
其中:
-
N: 样本数量
-
n: 当前样本
-
k: 当前频率,其中 k∈[0,N−1]
-
xn: 样本 n 的正弦值
-
Xk: 包含幅度和相位信息的 DFT
DFT(Xk)的输出是一个复杂数数组,包含频率成分的信息 [2]。直接应用 DFT 到信号上需要进行复杂的计算。幸运的是,已经开发了快速傅里叶变换(FFT) [3] 来提供 DFT 的更快实现。FFT 利用了 DFT 输出的对称性。我们将不会进一步讨论 FFT 的工作原理,因为它类似于 DFT 的标准实际应用。如果你需要更多细节,请参见 [3]。
代码示例:
我们将简单地开始理解我们在这篇文章中使用的每种方法的输入和输出。首先,我们将导入所需的包。Numpy 用于处理矩阵和计算。我们从 scipy.fft 模块(fft、rfft、fftfreq、rfftfreq)中导入帮助我们进行傅里叶分析相关计算的方法。最后,Plotly 和 matplotlib 用于可视化。
让我们开始吧…
# Import the required packages
import numpy as np
from scipy.fft import fft, rfft
from scipy.fft import fftfreq, rfftfreq
import plotly.graph_objs as go
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
%matplotlib inline
我们需要信号来测试我们的代码。正弦波非常适合我们的示例。在接下来的代码中,我们使用一个名为 Signal 的类生成正弦信号,你可以在以下GitHub gist中找到现成的。我们将使用这个类生成的信号包含三个正弦波(1、10、20)Hz,幅度分别为(3、1、0.5)。采样率将是 200,信号的持续时间为 2 秒。
# Generate the three signals using Signal class and its method sine()
signal_1hz = Signal(amplitude=3, frequency=1, sampling_rate=200, duration=2)
sine_1hz = signal_1hz.sine()
signal_20hz = Signal(amplitude=1, frequency=20, sampling_rate=200, duration=2)
sine_20hz = signal_20hz.sine()
signal_10hz = Signal(amplitude=0.5, frequency=10, sampling_rate=200, duration=2)
sine_10hz = signal_10hz.sine()
# Sum the three signals to output the signal we want to analyze
signal = sine_1hz + sine_20hz + sine_10hz
# Plot the signal
plt.plot(signal_1hz.time_axis, signal, 'b')
plt.xlabel('Time [sec]')
plt.ylabel('Amplitude')
plt.title('Sum of three signals')
plt.show()
我们接下来要处理的信号。[作者提供的图片]
该信号的傅里叶变换可以使用 scipy 包中的(fft)计算,如下所示 [4]:
# Apply the FFT on the signal
fourier = fft(signal)
# Plot the result (the spectrum |Xk|)
plt.plot(np.abs(fourier))
plt.show()
信号的 FFT 输出。[作者提供的图片]
上面的图应表示信号的频谱。注意,x 轴是样本数量(而不是频率分量),y 轴应表示正弦波的幅度。为了获得实际的频谱幅度,我们必须将(fft)的输出除以 N/2(样本数量)。
# Calculate N/2 to normalize the FFT output
N = len(signal)
normalize = N/2
# Plot the normalized FFT (|Xk|)/(N/2)
plt.plot(np.abs(fourier)/normalize)
plt.ylabel('Amplitude')
plt.xlabel('Samples')
plt.title('Normalized FFT Spectrum')
plt.show()
正常化后的 FFT 输出。[作者提供的图片]
要获取频率分量(x 轴),可以使用 scipy 包中的(fftfreq)。该方法需要样本数量(N)和采样率作为输入参数。它返回一个包含 N 个频率分量的频率轴 [5]。
# Get the frequency components of the spectrum
sampling_rate = 200.0 # It's used as a sample spacing
frequency_axis = fftfreq(N, d=1.0/sampling_rate)
norm_amplitude = np.abs(fourier)/normalize
# Plot the results
plt.plot(frequency_axis, norm_amplitude)
plt.xlabel('Frequency[Hz]')
plt.ylabel('Amplitude')
plt.title('Spectrum')
plt.show()
实际幅度和频率轴的频谱。[作者提供的图片]
为了理解上一段代码发生了什么,让我们仅绘制频率轴:
# Plot the frequency axis for more explanation
plt.plot(frequency_axis)
plt.ylabel('Frequency[Hz]')
plt.title('Frequency Axis')
plt.show()
频率轴。[作者提供的图片]
注意频率数组从零开始。然后,它以(d)的步长逐步增加,直到达到最大值(100Hz)。之后,它从负最大频率(-100Hz)开始,逐步增加回到正频率。可以从信号中获取信息的最大频率(100Hz)是采样率的一半,这符合采样定理 [2]。
由于实值信号频谱的对称性,我们只关注频谱的前半部分 [2]。Scipy 包提供了处理实值信号傅里叶变换的方法,其中利用了频谱的对称性质。这些方法包括(rfft [6],rfftfreq [7])。这些方法分别对应(fft,fftfreq)。通过比较相同信号上(fft)和(rfft)方法的时间执行,你会发现(rfft)稍微快一点。处理实值信号时,通常情况下,使用(rfft)是最佳选择。
# Calculate the time execution of (fft)
print('Execution time of fft function:')
%timeit fft(signal)
# Calculate the time execution of (rfft)
print('\nExecution time of rfft function:')
%timeit rfft(signal)
Execution time of fft function:
13.5 µs ± 8.3 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Execution time of rfft function:
12.3 µs ± 3.55 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
为总结我们关于缩放幅度和生成具有对称性质的实值信号频谱频率轴的讨论,下面的代码表示频谱的最终形式(右侧频率上的实际幅度)。
# Plot the actual spectrum of the signal
plt.plot(rfftfreq(N, d=1/sampling_rate), 2*np.abs(rfft(signal))/N)
plt.title('Spectrum')
plt.xlabel('Frequency[Hz]')
plt.ylabel('Amplitude')
plt.show()
信号的实际频谱。[作者提供的图片]
下面的图帮助你理解和记住如何获取频率轴和构成频谱的正弦波的实际幅度。
花点时间阅读每一行代码,它为你提供了信号的实际频谱。[作者提供的图片]
最终代码
现在我们已经理解了在傅里叶分析中使用的每种方法的输入和输出,让我们进行最终的代码编写。我们将构建一个类(Fourier),使我们使用傅里叶变换更方便和更易于使用。我们需要的类应该计算信号数据的 DFT 并直观地可视化数据。确保阅读该类的文档以理解其使用方法。如果你不熟悉 Python 中的类及其构建方法,请参阅之前的 帖子,了解如何构建类以生成信号。
# Building a class Fourier for better use of Fourier Analysis.
class Fourier:
"""
Apply the Discrete Fourier Transform (DFT) on the signal using the Fast Fourier
Transform (FFT) from the scipy package.
Example:
fourier = Fourier(signal, sampling_rate=2000.0)
"""
def __init__(self, signal, sampling_rate):
"""
Initialize the Fourier class.
Args:
signal (np.ndarray): The samples of the signal
sampling_rate (float): The sampling per second of the signal
Additional parameters,which are required to generate Fourier calculations, are
calculated and defined to be initialized here too:
time_step (float): 1.0/sampling_rate
time_axis (np.ndarray): Generate the time axis from the duration and
the time_step of the signal. The time axis is
for better representation of the signal.
duration (float): The duration of the signal in seconds.
frequencies (numpy.ndarray): The frequency axis to generate the spectrum.
fourier (numpy.ndarray): The DFT using rfft from the scipy package.
"""
self.signal = signal
self.sampling_rate = sampling_rate
self.time_step = 1.0/self.sampling_rate
self.duration = len(self.signal)/self.sampling_rate
self.time_axis = np.arange(0, self.duration, self.time_step)
self.frequencies = rfftfreq(len(self.signal), d = self.time_step)
self.fourier = rfft(self.signal)
# Generate the actual amplitudes of the spectrum
def amplitude(self):
"""
Method of Fourier
Returns:
numpy.ndarray of the actual amplitudes of the sinusoids.
"""
return 2*np.abs(self.fourier)/len(self.signal)
# Generate the phase information from the output of rfft
def phase(self, degree = False):
"""
Method of Fourier
Args:
degree: To choose the type of phase representation (Radian, Degree).
By default, it's in radian.
Returns:
numpy.ndarray of the phase information of the Fourier output.
"""
return np.angle(self.fourier, deg = degree)
# Plot the spectrum
def plot_spectrum(self, interactive=False):
"""
Plot the Spectrum (Frequency Domain) of the signal either using the matplotlib
package, or plot it interactive using the plotly package.
Args:
interactive: To choose if you want the plot interactive (True), or not
(False). The default is the spectrum non-interactive.
Retruns:
A plot of the spectrum.
"""
# When the argument interactive is set to True:
if interactive:
self.trace = go.Line(x=self.frequencies, y=self.amplitude())
self.data = [self.trace]
self.layout = go.Layout(title=dict(text='Spectrum',
x=0.5,
xanchor='center',
yanchor='top',
font=dict(size=25, family='Arial, bold')),
xaxis=dict(title='Frequency[Hz]'),
yaxis=dict(title='Amplitude'))
self.fig = go.Figure(data=self.data, layout=self.layout)
return self.fig.show()
# When the argument interactive is set to False:
else:
plt.figure(figsize = (10,6))
plt.plot(self.frequencies, self.amplitude())
plt.title('Spectrum')
plt.ylabel('Amplitude')
plt.xlabel('Frequency[Hz]')
# Plot the Signal and the Spectrum interactively
def plot_time_frequency(self, t_ylabel="Amplitude", f_ylabel="Amplitude",
t_title="Signal (Time Domain)",
f_title="Spectrum (Frequency Domain)"):
"""
Plot the Signal in Time Domain and Frequency Domain using plotly.
Args:
t_ylabel (String): Label of the y-axis in Time-Domain
f_ylabel (String): Label of the y-axis in Frequency-Domain
t_title (String): Title of the Time-Domain plot
f_title (String): Title of the Frequency-Domain plot
Returns:
Two figures: the first is the time-domain, and the second is the
frequency-domain.
"""
# The Signal (Time-Domain)
self.time_trace = go.Line(x=self.time_axis, y=self.signal)
self.time_domain = [self.time_trace]
self.layout = go.Layout(title=dict(text=t_title,
x=0.5,
xanchor='center',
yanchor='top',
font=dict(size=25, family='Arial, bold')),
xaxis=dict(title='Time[sec]'),
yaxis=dict(title=t_ylabel),
width=1000,
height=400)
fig = go.Figure(data=self.time_domain, layout=self.layout)
fig.show()
# The Spectrum (Frequency-Domain)
self.freq_trace = go.Line(x=self.frequencies, y=self.amplitude())
self.frequency_domain = [self.freq_trace]
self.layout = go.Layout(title=dict(text=f_title,
x=0.5,
xanchor='center',
yanchor='top',
font=dict(size=25, family='Arial, bold')),
xaxis=dict(title='Frequency[Hz]'),
yaxis=dict(title=f_ylabel),
width=1000,
height=400)
fig = go.Figure(data=self.frequency_domain, layout=self.layout)
fig.show()
让我们在上面的信号上测试我们的类。输入参数是实值信号数据(时间域)和该信号的采样率。
# Apply the DFT using the class Fourier
fourier = Fourier(signal, sampling_rate=200)
# Plot the spectrum interactively using the class Fourier
fourier.plot_spectrum(interactive=True)
使用我们类(Fourier)的信号频谱。如果你应用上述代码,可以通过悬停在蓝色线条上互动获取数值。[作者提供的图片]
心电图的频谱
我们的最终示例将是现实世界的信号数据。我们将使用 Fourier 类绘制心电图(ECG)的时频域。该信号为心脏电活动的 5 分钟长,采样频率为 360Hz。[8]
# Import the ECG signal from scipy package
from scipy.misc import electrocardiogram
# Built-in ECG signal
ecg = electrocardiogram()
# DFT using the class Fourier
ecg_spectrum = Fourier(signal = ecg, sampling_rate = 360.0)
# Plot the time-frequency domains of the ECG signal
ecg_spectrum.plot_time_frequency(t_title="ECG Signal", f_title="ECG Spectrum",
t_ylabel="Amplitude[mV]")
ECG 信号及其频谱。[图片由作者提供]
上面的图像展示了我们在本帖中构建的 Fourier 类的一个示例(心电图信号及其频谱)。使用 Plotly 包使你可以轻松地悬停查看图表的值并对有趣的部分进行缩放。
结论
-
我们在数学上介绍了离散傅里叶变换(DFT)。
-
讨论了逐步傅里叶分析编码。我们首先介绍了快速傅里叶变换(FFT)及其在 Python 中的实现,以生成信号的谱。
-
我们介绍了标准化频谱的要求,以便获得正弦波的实际幅度。此外,我们还使用了 scipy.fft 的一个辅助函数来生成频谱的频率轴(fftfreq)。
-
我们指出了傅里叶变换的对称性质以及谱在采样频率周围的对称性。这就是我们讨论处理实值信号数据的新方法(rfft,rfftfreq)的原因。
-
我们建立了一个类来简化傅里叶变换的使用,并使用 Plotly 包交互式地生成信号的频域。
感谢阅读 _
参考文献
[1] Dongarra, J., & Sullivan, F. (2000)。十大算法的客座编辑介绍。科学与工程计算,2(01),22–23。
[2] Kong, Q., Siauw, T., & Bayen, A. (2020)。傅里叶变换。Python 编程与数值方法:工程师和科学家的指南(第 415–444 页)。学术出版社。
[3] Cooley, J. W., & Tukey, J. W. (1965)。复杂傅里叶级数的机器计算算法。计算数学,19(90),297–301。
[4] Scipy 文档,API 参考,离散傅里叶变换 (scipy.fft.fft)。[访问日期:2023 年 2 月 23 日]
[5] Scipy 文档,API 参考,离散傅里叶变换 (scipy.fft.fftfreq)。[访问日期:2023 年 2 月 23 日]
[6] Scipy 文档,API 参考,离散傅里叶变换 (scipy.fft.rfft)。[访问日期:2023 年 2 月 23 日]
[7] Scipy 文档,API 参考,离散傅里叶变换 (scipy.fft.rfftfreq)。 [访问于 2023 年 2 月 23 日]
[8] Scipy 文档,API 参考,杂项例程 (scipy.misc.electrocardiogram)。 [访问于 2023 年 2 月 23 日]
用实体解析和图神经网络进行欺诈检测
一本关于如何利用实体解析提升机器学习检测欺诈的实用指南
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 8 月 24 日
--
由作者使用 Bing Image Creator 生成的图神经网络的表示
在线欺诈对金融、电子商务及其他相关行业是一个不断增长的问题。为了应对这一威胁,组织利用基于机器学习和行为分析的欺诈检测机制。这些技术能够实时检测到异常模式、异常行为和欺诈活动。
不幸的是,通常只考虑当前交易,例如一个订单,或者流程仅基于客户档案中的历史数据,该档案通过客户 ID 进行识别。然而,专业的欺诈者可能会使用低价值的交易来创建客户档案,从而建立其档案的正面形象。此外,他们可能会同时创建多个类似的档案。只有在欺诈发生后,受攻击的公司才会意识到这些客户档案彼此之间的关系。
使用实体解析可以轻松地将不同的客户档案合并为一个 360° 客户视图,从而看到所有历史交易的完整情况。在机器学习中使用这些数据,例如使用神经网络或简单的线性回归,已经可以为最终模型提供额外的价值,而真正的价值在于还要查看各个交易之间是如何相互连接的。这就是图神经网络(GNN)发挥作用的地方。除了查看从交易记录中提取的特征外,它们还提供了查看从图边生成的特征(交易如何相互链接)或仅仅是实体图的一般布局的可能性。
示例数据
在我们深入细节之前,我有一个声明要在这里说明:我是一名开发者和实体解析专家,而不是数据科学家或 ML 专家。虽然我认为总体方法是正确的,但我可能没有遵循最佳实践,也不能解释某些方面,例如隐藏节点的数量。请将本文作为灵感来源,并根据自己的经验来考虑 GNN 的布局或配置。
本文旨在聚焦于从实体图布局中获得的见解。为此,我创建了一个小型 Golang 脚本,用于生成实体。每个实体被标记为欺诈或非欺诈,并由记录(订单)和边(这些订单如何链接)组成。请参见以下单个实体的示例:
{
"fraud":1,
"records":[
{
"id":0,
"totalValue":85,
"items":2
},
{
"id":1,
"totalValue":31,
"items":4
},
{
"id":2,
"totalValue":20,
"items":9
}
],
"edges":[
{
"a":1,
"b":0,
"R1":1,
"R2":1
},
{
"a":2,
"b":1,
"R1":0,
"R2":1
}
]
}
每条记录有两个(潜在的)特征,总值和购买的项目数量。然而,生成脚本完全随机化了这些值,因此它们在猜测欺诈标签时不会提供价值。每条边还包含两个特征 R1 和 R2。例如,这些特征可能表示记录 A 和 B 是否通过类似的姓名和地址(R1)或通过类似的电子邮件地址(R2)进行链接。此外,我故意省略了所有与本示例无关的属性(如姓名、地址、电子邮件、电话号码等),这些属性通常在实体解析过程之前是相关的。由于 R1 和 R2 也是随机化的,它们也不会为 GNN 提供价值。然而,根据欺诈标签,边的布局有两种可能方式:星形布局(fraud=0)或随机布局(fraud=1)。
这个想法是非欺诈客户更可能提供准确匹配的相关数据,通常是相同的地址和名字,仅有少量拼写错误。因此,新交易可能会被识别为重复。
去重实体(图片由作者提供)
欺诈客户可能会想掩盖他们仍然是计算机背后的同一个人,使用各种名字和地址。然而,实体解析工具仍然可能识别出相似性(例如地理和时间相似性、电子邮件地址中的重复模式、设备 ID 等),但实体图可能看起来更复杂。
复杂的、可能是欺诈性的实体(图片由作者提供)
为了让它不那么简单,生成脚本还具有 5%的错误率,这意味着当实体具有星形布局时,会被标记为欺诈,而随机布局则被标记为非欺诈。此外,还有一些情况下数据不足以确定实际布局(例如只有一两条记录)。
{
"fraud":1,
"records":[
{
"id":0,
"totalValue":85,
"items":5
}
],
"edges":[
]
}
实际上,你很可能会从所有三种特征(记录属性、边属性和边布局)中获得有价值的见解。以下代码示例将考虑这一点,但生成的数据则没有。
创建数据集
示例使用了 python(数据生成除外)和DGL,并且使用了pytorch 后台。你可以在github上找到完整的 jupyter 笔记本、数据和生成脚本。
让我们从导入数据集开始:
import os
os.environ["DGLBACKEND"] = "pytorch"
import pandas as pd
import torch
import dgl
from dgl.data import DGLDataset
class EntitiesDataset(DGLDataset):
def __init__(self, entitiesFile):
self.entitiesFile = entitiesFile
super().__init__(name="entities")
def process(self):
entities = pd.read_json(self.entitiesFile, lines=1)
self.graphs = []
self.labels = []
for _, entity in entities.iterrows():
a = []
b = []
r1_feat = []
r2_feat = []
for edge in entity["edges"]:
a.append(edge["a"])
b.append(edge["b"])
r1_feat.append(edge["R1"])
r2_feat.append(edge["R2"])
a = torch.LongTensor(a)
b = torch.LongTensor(b)
edge_features = torch.LongTensor([r1_feat, r2_feat]).t()
node_feat = [[node["totalValue"], node["items"]] for node in entity["records"]]
node_features = torch.tensor(node_feat)
g = dgl.graph((a, b), num_nodes=len(entity["records"]))
g.edata["feat"] = edge_features
g.ndata["feat"] = node_features
g = dgl.add_self_loop(g)
self.graphs.append(g)
self.labels.append(entity["fraud"])
self.labels = torch.LongTensor(self.labels)
def __getitem__(self, i):
return self.graphs[i], self.labels[i]
def __len__(self):
return len(self.graphs)
dataset = EntitiesDataset("./entities.jsonl")
print(dataset)
print(dataset[0])
这处理了实体文件,这是一个 JSON 行文件,每行表示一个实体。在迭代每个实体时,它生成边特征(形状为[e, 2]的长张量,e=边数)和节点特征(形状为[n, 2]的长张量,n=节点数)。然后,它根据 a 和 b(每个形状为[e, 1]的长张量)构建图,并将边和图特征分配给该图。所有生成的图都被添加到数据集中。
模型架构
现在我们已经准备好数据了,我们需要考虑 GNN 的架构。这是我想到的,但可能需要根据实际需求进行更多调整:
import torch.nn as nn
import torch.nn.functional as F
from dgl.nn import NNConv, SAGEConv
class EntityGraphModule(nn.Module):
def __init__(self, node_in_feats, edge_in_feats, h_feats, num_classes):
super(EntityGraphModule, self).__init__()
lin = nn.Linear(edge_in_feats, node_in_feats * h_feats)
edge_func = lambda e_feat: lin(e_feat)
self.conv1 = NNConv(node_in_feats, h_feats, edge_func)
self.conv2 = SAGEConv(h_feats, num_classes, "pool")
def forward(self, g, node_features, edge_features):
h = self.conv1(g, node_features, edge_features)
h = F.relu(h)
h = self.conv2(g, h)
g.ndata["h"] = h
return dgl.mean_nodes(g, "h")
构造函数接受节点特征的数量、边特征的数量、隐藏节点的数量和标签(类别)的数量。然后创建两个层次:NNConv 层,根据边和节点特征计算隐藏节点,然后是GraphSAGE 层,根据隐藏节点计算结果标签。
训练和测试
快完成了。接下来我们准备数据进行训练和测试。
from torch.utils.data.sampler import SubsetRandomSampler
from dgl.dataloading import GraphDataLoader
num_examples = len(dataset)
num_train = int(num_examples * 0.8)
train_sampler = SubsetRandomSampler(torch.arange(num_train))
test_sampler = SubsetRandomSampler(torch.arange(num_train, num_examples))
train_dataloader = GraphDataLoader(
dataset, sampler=train_sampler, batch_size=5, drop_last=False
)
test_dataloader = GraphDataLoader(
dataset, sampler=test_sampler, batch_size=5, drop_last=False
)
我们使用 80/20 的比例进行随机抽样,并为每个样本创建数据加载器。
最后一步是用我们的数据初始化模型,进行训练,然后测试结果。
h_feats = 64
learn_iterations = 50
learn_rate = 0.01
model = EntityGraphModule(
dataset.graphs[0].ndata["feat"].shape[1],
dataset.graphs[0].edata["feat"].shape[1],
h_feats,
dataset.labels.max().item() + 1
)
optimizer = torch.optim.Adam(model.parameters(), lr=learn_rate)
for _ in range(learn_iterations):
for batched_graph, labels in train_dataloader:
pred = model(batched_graph, batched_graph.ndata["feat"].float(), batched_graph.edata["feat"].float())
loss = F.cross_entropy(pred, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
num_correct = 0
num_tests = 0
for batched_graph, labels in test_dataloader:
pred = model(batched_graph, batched_graph.ndata["feat"].float(), batched_graph.edata["feat"].float())
num_correct += (pred.argmax(1) == labels).sum().item()
num_tests += len(labels)
acc = num_correct / num_tests
print("Test accuracy:", acc)
我们通过提供节点和边缘的特征大小(在我们的例子中均为 2),隐藏节点(64)以及标签数量(2,因为只有欺诈或非欺诈)来初始化模型。然后,优化器以 0.01 的学习率初始化。之后我们运行总共 50 次训练迭代。一旦训练完成,我们使用测试数据加载器测试结果,并打印出结果准确率。
对于各种运行,我的典型准确率在 70%到 85%之间。然而,也有一些例外情况,准确率降到大约 55%。
结论
鉴于我们示例数据集中唯一可用的信息是节点如何连接的解释,初步结果看起来非常有前景,并且建议使用真实世界数据和更多训练可能会实现更高的准确率。
显然,在处理真实数据时,布局并不那么一致,也没有明显的布局与欺诈行为之间的关联。因此,你还应该考虑边缘和节点的特征。本文的关键要点应是实体解析提供了使用图神经网络进行欺诈检测的理想数据,并且应视为欺诈检测工程师工具库的一部分。
最初发布于 https://tilores.io.
无限制:在 MoMA 机器幻觉的验证
揭示了人类与 AI 艺术合作的幕后创作过程
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 8 月 31 日
--
照片由 Jamison McAndie 在 Unsplash 上拍摄
自 1929 年以来,位于纽约市的现代艺术博物馆 (MoMA) 一直是艺术爱好者的圣地。它是一座灯塔,照亮前卫的绘画和雕塑,由于“现代艺术”的定义不断变化,它的收藏也在不断演变。如今,这个杰出的机构正在验证数字艺术。
作为 Refik Anadol Studio (RAS) 的首席数据科学家,与 Refik Anadol 合作,我非常激动地看到我们的作品《无监督》被 MoMA 接纳。
在 RAS,我们将数据美学带给更广泛的公众,展示了 AI 的潜力超越了文本生成。我们生活在见证我们艺术的人类影响力 — 它如何在情感层面上影响各个年龄段和背景的人。这是一种共同的人类体验,并且非常易于接触。
“Unsupervised” 由 gottalovenewyork 拍摄于 YouTube
AI 生成的艺术当然也并非没有争议。其中一个最广泛的误解是,数字艺术通常以及 AI 生成的艺术特别不被视为真正的艺术作品。然而,即便是 AI 生成的艺术也不是完全由机器创造的。它需要人类的触感。作为《无监督》的创意背后的人,Anadol 从原始数据中创作艺术。 这在数字艺术中是新的。之前的艺术家们利用数据跟随模板来制作已经存在的东西的复制品。Refik 的作品则完全不同。
想象机器的幻觉
在 RAS,我领导着一个由七名数据科学家组成的团队。我的工作日充满了监督、审查和编写代码,同时还直接与客户对接和进行项目规划。虽然这看起来不太艺术化,但迄今为止,我已经收集了超过三十亿张图像,用于推动 AI 生成艺术的创作。鉴于我每天都被编码和数据集的细节所包围,退后一步看到 RAS 创作的整体作品是一种令人惊叹的体验。
让我带你体验一下《无监督》的感受。 想象一下:你走进了 MoMA 的大厅。起初,你会觉得自己走进了任何其他艺术博物馆。但如果你环顾四周,你会突然被这个巨大的屏幕(24 英尺乘 24 英尺)的景象所震撼,周围坐着和站着的人们 — 都在凝视这个展览。
展览本身不断移动。它持续变化,展示出迷人的颜色和形状。你所看到的取决于你进入 MoMA 时偶遇展览的哪个章节,以及来自大厅的实时音频、运动跟踪和天气数据。
Christian Burke 站在 MoMA 展览前
“无监督”试图回答这个问题:“如果一台机器亲自体验 MoMA 的收藏,它会梦到什么或产生什么幻觉?”通过将 MoMA 所有收藏的数据结合起来并推断形成这些机器梦境,“无监督”带领观众穿越艺术的历史,并将聚光灯投射到艺术的潜在未来。
艺术有时努力触及更广泛的社会问题。如果你想从“无监督”中获得一个普遍的启示,那就是展览表明 AI 生成的数字艺术在合法化方面的一个转折点。MoMA 对艺术界的意义就像核聚变对物理学家一样——一种圣杯。MoMA 选择展示这项关于计算机如何处理数据——如何“思考”、创造和产生幻觉——的探索,验证了 Anadol 和其他数字艺术家的工作。
但并不是每个参观“无监督”的人都在思考机器和它们的梦境。当你走进 MoMA 的大厅时,你会看到人类的多样性——从跑来跑去的小孩子到年长的人和各行各业的人们——都在享受这种强烈的共同体验。看人们观看展览对我来说和看“无监督”本身一样激动。我见过有人哭泣,也见过喜悦和爱的表情。我自己不是艺术家,但我相信它具有治愈的特质。我也相信,只要你足够关注,任何人在任何地方做的事情中都可以找到艺术。编写代码中甚至也可以存在艺术。
人类与 AI 的合作关系
人类艺术家需要技术技能来创作艺术。他们需要理解诸如色调值呈现、透视、对称,甚至人体解剖等概念。“无监督”将艺术的技术方面向前推进了一大步,通过创造人类与 AI 之间的合作伙伴关系。
RAS 利用来自 MoMA 超过 180,000 件艺术作品的数据创建了“无监督”项目。我们将 Warhol、Picasso、Boccioni 的作品以及 Pac-Man 的图像都输入了软件中。然后我们创建了各种 AI 模型并进行了广泛的测试。选择出最佳模型后,我们训练它不仅仅是将所有输入的艺术作品进行合成,而是创造出一种全新的东西。
“无监督”不仅仅是其组成部分的总和;它是完全新的东西。 由于我们的艺术处理,展览中创造的一切都是原创的。
人类与机器之间的合作关系需要在硬件和软件方面进行新的创新。我们的团队在创建所需的神经网络并使展览能够实时持续变换其图像,响应独特的环境因素时面临了许多挑战。
Unsupervised 的静态图像,MoMA
一个挑战是分辨率。如果你在 Stable Diffusion 中输入一个提示,你通常会得到 512 x 512 像素的分辨率。我们使用的 AI 基础——Nvidia 的 StyleGAN——通常提供 1024 x 1024 的分辨率。“Unsupervised”的分辨率是 3840 x 3960,这可能是神经网络合成图像的最高分辨率。当你走进 MoMA 的大厅看到“Unsupervised”时,你会明白高分辨率为何如此重要。它让艺术栩栩如生,让它看起来几乎像一个可以从屏幕上跳出来的生物。
实时性是另一个需要克服的重要挑战。“Unsupervised”以液态流畅性产生其机器幻觉和梦想。这些机器幻觉源自合成超过 180,000 件艺术作品,并考虑实时因素。
距离 MoMA 不远的一栋建筑有一个气象站,收集天气相关数据。 我们将这些数据输入“Unsupervised”,这意味着无论何时是阴天、晴天、雨天还是雾天,机器都会将外部世界的氛围融入其室内展示中。
其次,展览融入了来自观众自身的实时数据。大厅天花板上的摄像头将有关访客数量及其动作的数据传输到机器中。机器在展示其艺术梦想时会考虑这些数据。
有一个古老的问题:生活是否更像艺术,还是艺术更像生活?对于“Unsupervised”,答案显然是两者兼而有之。
即使展览的观众被展品所感动,他们自己也会影响“Unsupervised”的呈现方式。
Irma Zandl 在 YouTube 上捕捉的 MoMA 展览的 Unsupervised 画面
类似地,AI 与人类之间的合作关系也存在双向街道的描述。有一种说法认为数字艺术涉及在传统艺术过程中加入一些额外的技术技能。然而,我更愿意把它看作是一个双向互动的过程。
数字艺术确实涉及将技术工具融入艺术过程,例如扩散模型和提示工程。另一方面,AI 本身消除了进入艺术世界的一些障碍。比如说,我喜欢绘画,但我画人的能力很差。AI 允许我弥补这一技术局限。
AI 的未来
由于受欢迎的需求,“Unsupervised”在 MoMA 的展出已多次延长,机器的幻觉可能会无限期地继续下去。 展望未来,我希望看到 AI 生成数字艺术的更大合法化。模型将继续改进,希望技术能够变得更加普及。
AI 可能是一种通过提高可访问性来使艺术世界民主化的手段,但目前仍存在技术障碍。我希望看到 AI 工具以更简单、更直观的界面出现,这样可以减少技术知识障碍。我们目前在 RAS 正在进行的一个新项目是网络集成工具,这将使人们更容易使用和互动 AI。这是我们在 RAS 的主要目标:创造与 AI 更大互动的手段。
由于“无监督”需要大量的人为干预,我有时被问及我是否认为 AI 总会需要这种人类干预。至少目前,答案肯定是肯定的。AI 在许多方面表现出色,比如合成,但在大规模工程和创新方面缺乏能力。
AI 生成的艺术可能看起来很有创意,但 AI 本身并不具备创意。实际上,它与创意正好相反。如果我们想在 AI 和科技领域不断前进和取得进展,我们需要依靠自己——而不是机器。
—
作者说明:MoMA 授权 Refik Anadol Studio (RAS) 使用他们的训练数据。
Christian Burke 领导 Refik Anadol Studio 的数据科学团队,该团队包括 AI、机器学习、网络和 Web3 开发。
你可以在 Twitter 和 LinkedIn 上关注 Christian。
频率学派与贝叶斯统计学的比较
原文:
towardsdatascience.com/frequentist-vs-bayesian-statistics-with-tensorflow-fbba2c6c9ae5
概率深度学习
·发表于 Towards Data Science ·阅读时长 10 分钟·2023 年 1 月 5 日
--
介绍
本文属于“概率深度学习”系列。该系列每周讨论概率方法在深度学习中的应用。主要目标是扩展深度学习模型以量化不确定性,即了解它们不知道什么。
频率学派统计方法基于重复采样和长期相对频率的理念。它涉及构建关于总体的假设,并使用样本数据对其进行检验。另一方面,贝叶斯方法基于主观概率,并涉及使用收集到的数据更新对总体的初步信念。这两种方法各有优缺点,选择使用哪一种取决于问题和分析目标。在本文中,我们将深入探讨频率学派与贝叶斯方法之间的差异,并讨论如何利用 TensorFlow 和 TensorFlow Probability 实现这两种方法。
迄今为止发布的文章:
-
从头开始在 TensorFlow 概率论中实现最大似然估计
-
从头开始在 TensorFlow 中实现概率线性回归
图 1:今天的座右铭:看待概率的方式没有唯一答案(来源)。
我们使用 TensorFlow 和 TensorFlow Probability 来开发我们的模型。TensorFlow Probability 是一个基于 TensorFlow 的 Python 库。我们将从 TensorFlow Probability 中可以找到的基本对象开始,并理解如何操作它们。接下来的几周我们将逐步增加复杂性,并将我们的概率模型与现代硬件(例如 GPU)上的深度学习结合起来。
如常,代码可以在我的GitHub上找到。
频率学与贝叶斯方法的比较
频率学统计和贝叶斯统计是统计推断的两种主要方法,统计推断是利用数据对总体得出结论的过程。这两种方法都用于估计未知量、进行预测和检验假设,但在概率的解释及如何融入先验知识和证据方面有所不同。
在频率学统计中,概率被解释为事件在无限次试验中的长期相对频率。这种方法基于一个观点,即总体参数的真实值是固定的,但未知且必须通过数据进行估计。在这个框架下,统计推断是通过对观察数据进行假设来从中得出的,使用点估计、置信区间和假设检验等技术。
另一方面,贝叶斯统计将概率解释为对事件的信念或确定性的程度。这种方法允许通过使用贝叶斯定理将先验知识和证据融入统计分析。在这个框架下,总体参数的真实值被视为随机变量,并随着新数据的收集而更新。这导致了一个关于参数空间的完整分布,称为后验分布,这可以用来进行概率预测并量化不确定性。
两种方法之间的一个关键区别是它们处理不确定性的方式。在频率学统计中,不确定性通过使用置信区间来量化,这些区间提供了基于观察数据的真实总体参数的可能范围的估计。在贝叶斯统计中,不确定性通过完整的后验分布来表示,这使得对参数真实值的不确定性进行更全面的描述成为可能。
另一个区别是贝叶斯统计允许结合先验知识,这在数据有限或数据生成过程复杂的情况下尤其有用。然而,先验分布的选择可以显著影响贝叶斯分析的结果,因此选择一个适合当前问题的先验非常重要。
问题
作为应用频率主义方法解决问题的一个例子,考虑基于样本数据估计总体收入均值的任务。在这种情况下,目标是使用样本数据推断总体的真实均值(我们暂时假设总体收入的标准差已知)。
让我们生成一些数据:
import tensorflow as tf
import tensorflow_probability as tfp
import matplotlib.pyplot as plt
from scipy.stats import t
import numpy as np
tfd = tfp.distributions
# Sample data
sample_size = 30.
sample_mean = 50000.
sample_stddev = 10000.
sample_data = tfd.Normal(loc=sample_mean, scale=sample_stddev).sample(sample_size)
plt.hist(sample_data, density=True, alpha=0.5);
图 2: 样本均值的直方图 — 合成生成的数据。
频率主义方法
使用频率主义方法解决此问题的一种方式是通过点估计。点估计涉及使用一个单一的点估计,例如样本均值,来代表未知的总体参数。
样本均值 𝜇̂ 是一个常用的总体均值 𝜇 的点估计。它计算为样本值的总和 𝑥1,𝑥2,…,𝑥𝑛 除以样本大小 𝑛:
sample_mean = tf.reduce_mean(sample_data)
sample_mean
<tf.Tensor: shape=(), dtype=float32, numpy=50212.75>
然而,仅凭点估计无法完整描述围绕估计值的不确定性。为了量化这种不确定性,我们可以使用置信区间。置信区间是基于观察数据对真实总体参数的可能范围的估计,通过从点估计中加减误差边际来构建。误差边际由所需的置信水平和样本大小决定,反映了样本数据的变异性。例如,95% 的置信区间表示我们有 95% 的信心真实总体参数落在该区间内。
总体均值的置信区间通过向点估计添加和减去误差边际 𝑚 来构建:
误差边际由所需的置信水平 1−𝛼 和样本大小 𝑛 决定。它反映了样本数据中的变异性,通常通过点估计的标准误差 𝑆𝐸 计算:
其中 𝑡1−𝛼2,𝑛−1 是具有 𝑛−1 自由度的 𝑡 分布的临界值,用于所需的置信水平。
样本均值的标准误差计算为样本标准差 𝜎 除以样本大小的平方根:
# Standard error of the sample mean
sample_stddev = tf.math.reduce_std(sample_data)
standard_error = sample_stddev / tf.sqrt(sample_size)
# Margin of error
confidence_level = 0.95
degrees_of_freedom = sample_size - 1
t_distribution = tfp.distributions.StudentT(df=degrees_of_freedom, loc=0., scale=1.)
# t_distribution.quantile() seems to have a bug
t_value = t.ppf(confidence_level+(1-confidence_level)/2, df=sample_size-1)
margin_of_error = t_value * standard_error
confidence_interval_lower = sample_mean - margin_of_error
confidence_interval_upper = sample_mean + margin_of_error
confidence_interval = (confidence_interval_lower.numpy(), confidence_interval_upper.numpy())
confidence_interval
(46550.523, 53645.047)
我们可以定义一个辅助函数,用于绘制我们预测的均值和置信区间,并将其叠加在采样数据上。
def visualize_output(sample_data, sample_mean, interval, type_interval):
plt.hist(sample_data, density=True, alpha=0.5)
plt.axvline(x=sample_mean, color='r', linestyle='dashed', linewidth=2)
plt.axvline(x=interval[0], color='g', linewidth=2)
plt.axvline(x=interval[1], color='g', linewidth=2)
plt.legend(['Sample Mean', f'{type_interval} interval'])
plt.show()
visualize_output(sample_data, sample_mean, confidence_interval, 'confidence')
图 3:使用频率学派方法估计的参数的均值和置信区间。
贝叶斯方法
作为应用贝叶斯方法的一个示例,考虑基于数据样本估计总体平均收入的任务。在这种情况下,目标是利用样本数据和任何可用的先验知识,对总体的真实平均收入做出推断。
使用贝叶斯方法解决这个问题的一种方式是通过使用贝叶斯定理,该定理允许我们根据观察到的数据更新对总体平均收入值的信念。贝叶斯定理指出,给定某些数据的假设的后验概率等于假设的先验概率乘以给定假设的数据的似然度,再除以数据的边际概率。数学表达式为:
其中𝜃是总体平均收入,𝑥是观察到的数据(即收入值的样本),𝑃(𝜃|𝑥), 𝑃(𝑥|𝜃), 𝑃(𝜃)和𝑃(𝑥)分别是后验概率、似然度、先验概率和边际概率。
要将这种方法应用于收入示例,我们必须首先指定一个关于总体平均收入的先验分布,𝑃(𝜃)。这个先验分布表示我们在观察到数据之前对总体平均收入值的信念。先验分布的选择将取决于任何可用的先验知识以及数据生成过程的特征。例如,如果我们有强有力的先验知识表明总体平均收入服从均值为 45,000 和标准差为 5,000 的正态分布,我们可以使用具有这些参数的正态先验分布。
mu_prior = 40000.
sigma_prior = 5000.
prior = tfd.Normal(loc=mu_prior, scale=sigma_prior)
似然函数,𝑃(𝑥|𝜃),表示在给定总体平均收入值𝜃的情况下观察到样本数据𝑥的概率。由于样本数据被假设为独立同分布(i.i.d.),似然函数仅为样本值的单个概率密度函数的乘积。例如,如果样本数据服从已知标准差为𝜎的正态分布,则似然函数是均值等于总体平均收入𝜃,标准差等于样本标准差的正态分布:
mu_likelihood = np.mean(sample_data)
sigma_likelihood = np.std(sample_data)
likelihood = tfd.Normal(loc=np.mean(sample_data), scale=np.std(sample_data))
接下来,我们使用贝叶斯定理计算后验分布:
后验分布可以用来进行概率预测,并量化围绕总体均值收入估计的不确定性。例如,我们可以利用后验分布来计算后验均值和后验标准差,分别作为对总体均值收入的估计以及对估计不确定性的量化。
如上所述,后验分布是通过两个高斯分布的乘积得出的。为了实现这一点,我们需要引入一个额外的概念——共轭分布。在我们的例子中,我们拥有正态-正态共轭族,这是一个参数化的分布族,其中先验分布和似然函数都是正态分布。这个模型类具有几个吸引人的特性,包括能够解析地计算后验分布以及最大后验(MAP)估计的封闭形式解。
后验分布被定义为给定观测数据𝑦的模型参数(在本例中为正态分布的均值𝜇和标准差𝜎)的分布。可以使用以下方程计算后验分布的均值和标准差:
其中𝜇0 和𝜎2_0 分别是先验分布的均值和方差,𝜇_𝑙和𝜎2_l是似然函数的均值和方差,𝑛是样本大小。
这些方程假设先验分布和似然函数都是正态分布,正如在正态-正态共轭模型中所见。如果先验和似然不是正态分布,或者后验分布没有封闭形式的解,这些参数可以通过马尔可夫链蒙特卡洛(MCMC)采样等技术进行近似。
我们准备实施我们的新方程。
# Compute the posterior distribution using Bayes' theorem
var_posterior = 1 / ((sample_size/(sigma_likelihood**2)) + (1/(sigma_prior**2)))
mu_posterior = var_posterior * (mu_prior/(sigma_prior**2) + (sample_size*mu_likelihood)/(sigma_likelihood**2))
posterior = tfd.Normal(mu_posterior, tf.sqrt(var_posterior))
credible_interval = (posterior.quantile(0.025).numpy(), posterior.quantile(0.975).numpy())
credible_interval
(45801.615148780984, 52224.89311258316)
我们可以看到,更新我们的先验信念后,得到了后验分布。
x = np.linspace(30000, 70000, 1000)
y_prior = prior.prob(x)
y_likelihood = likelihood.prob(x)
y_posterior = posterior.prob(x)
plt.plot(x,y_prior, label="Prior")
plt.plot(x,y_likelihood, label="Likelihood")
plt.plot(x,y_posterior, label="Posterior")
plt.legend();
图 4:先验、似然和后验概率函数。
最后,与频率主义方法相同,我们可以绘制总体均值的估计值。请注意,我们实际上是在用贝叶斯方法计算置信区间,而不是置信区间。
visualize_output(sample_data, posterior.mean(), (posterior.quantile(0.025), posterior.quantile(0.975)), 'credible')
图 5:使用贝叶斯方法估计参数的均值和置信区间
结论
总之,频率主义和贝叶斯方法是分析数据和做出预测的两种不同方式。频率主义方法基于统计显著性的理念,涉及构建关于总体的假设并使用样本数据进行检验。贝叶斯方法则基于主观概率,通过使用收集的数据来更新对总体的初步信念。
使用 TensorFlow 和 TensorFlow Probability,我们展示了如何计算频率学派方法的均值和置信区间,以及贝叶斯方法的均值和可信区间。这两种方法在不同类型的问题中都可能有用,因此了解每种方法的优缺点对于选择最适合的方案至关重要。
保持联系:LinkedIn
从算法到文字:我为何写作关于机器学习
原文:
towardsdatascience.com/from-algorithms-to-words-why-i-write-about-machine-learning-36e705e1fe54
在线写作对我来说是一个重大改变(这也可以成为你的改变)
·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 2 月 6 日
--
图片由 Midjourney 生成
我已经写了将近半个十年关于机器学习的文章。我在 Medium 上获得了超过 2K 的关注者,并且最近在 Gumroad 上推出了我的第一本电子书。工作中的新同事有时也会因为我的博客而认出我。
反思时间:我为什么要做这些?为什么要写关于机器学习的文章?这对我有什么好处?为什么这也可能对你有帮助?
剧透:这不仅仅是为了赚钱。如果是的话,我可能会推出 在 Kindle Direct Publishing 上的着色书 。这也不是为了增加像 Medium 上的关注者或“点赞”这样的虚假数字。虽然这些很好,但它们不能告诉我人们是否真正关心我的工作。
为什么写关于机器学习的文章?对我来说,这不仅仅是一个原因,而是 5 个原因。以下是这些原因。
1 — 我写作是为了学习
理查德·费曼常说,如果你不能用简单的术语向别人解释一个概念,那你自己也还没有真正理解它。因此,学习新知识的最佳方式就是教别人,这个理念被称为费曼技巧。
这就是我在博客中做的事情。我写的每一篇文章实际上都是我在教授的新的课程——因此也是我在学习——关于机器学习的不同方面,例如
-
学习排序:入门指南
-
机器学习问题中的类别不平衡:实用指南
-
A/B 测试的乐趣:理论、实践与陷阱
我写作是为了学习。而我学习是为了保持相关性并保持技能的敏锐。这在如机器学习这样多样且快速变化的领域尤为重要。
2 — 我写作是为了提高知名度
在线写作,并在 LinkedIn 上分享我的工作,赋予了我作为领域专家的某种信誉。这为我的在线个人资料带来了其他方式无法获得的可见性。
这种知名度过去为我打开了几个职业机会,包括 2019 年在亚马逊担任应用科学家的职位,以及我在 2022 年转职成为 Meta 的机器学习工程师。在这两个案例中,招聘人员都是在网上发现我的,而不是反过来。
设想一下你作为招聘人员在网上寻找机器学习专业人士:如果 100 个个人资料匹配某个特定职位,但其中一个还是写作内容创作者,写关于机器学习的文章,你认为他们会先联系谁?
在线写作使你从人群中脱颖而出。有时候,这意味着你的 LinkedIn 个人资料可能只是被浏览一下,而不是收到非正式电话面试的邀请。
3 — 我写作是为了建立额外的收入来源
额外的收入来源是一种保障。正如我们在 2022/2023 年大规模科技裁员中看到的那样,没有任何工作是安全的,即使是高大上的机器学习职位也不例外。我的写作目标之一一直是创造多个额外的收入来源,使自己不那么依赖单一薪水。
Medium 非常适合这个目的。它根据读者在文章上花费的时间支付给作者版税,类似于 Spotify 支付给艺术家一样。此外,如果你的写作质量高,Medium 的推荐算法会将其分发给更多读者,从而创造更多互动和版税。
这并不是说在 Medium 上赚钱很容易。这是一项艰苦的工作,需要数年才能掌握。你必须付出努力。
最初,我每月的收入仅有令人沮丧的几美分,但随着我不断学习、尝试新事物和改进,这个数字逐渐增长。我最受欢迎的文章,“算法是不够的”,让我赚到了近 500 美元的版税。每周有 2 篇这样的文章,你就开始看到相当可观的收入来源。
4 — 我写作是为了提高我的写作水平
写作良好是技术行业成功的最重要技能之一,尤其是在像亚马逊这样的公司中,书面叙事文化非常强烈。杰夫·贝索斯著名地禁止在领导会议中使用 PowerPoint,改为使用密集的叙述。
COVID-19 放大了成为一个优秀写作者的需求。如今,大多数协作不是在办公室同步进行,而是异步和远程进行的。坐在我位于西雅图的家庭办公室,我可能与来自湾区、纽约市和英国的一些合作者合作,而让每个人都在同一页面上的最佳方式就是使用包含书面叙述的共享文档。良好的写作已经成为后疫情时代混合/远程工作环境中协作的基石。
良好的写作在绩效评估季节也会有所帮助。如果你习惯了写作,那么将你一年的成就浓缩成一页纸应该是小菜一碟。
像任何事情一样,写作是一种技能。你写得越多,你就会写得越好,你也会从中获得更多的好处。
5 — 我写作是因为我喜欢它
最后,但也许最重要的是,如果我不享受写作,我就不会写。看到从一张白纸开始并填充内容的过程对我来说是令人满意的,更重要的是当我看到用户如何与这些内容互动时。我喜欢这种创造性的工作。
简而言之,如果我不享受写作,它就不会成为我一个可持续的副业——其他任何理由都不足以支撑它。就像生活中的任何事情一样,热情是持续下去的最重要因素。
结语:迈向机器学习素养
如果你是一名机器学习专业人士并且想要
-
建立一致的学习习惯,
-
在网上变得更加显眼(包括对招聘人员),
-
更好地清晰简洁地表达你的想法,并且
-
建立额外的收入来源,
考虑在网上写关于你工艺的文章。这对我来说是一个游戏改变,对你也可能是一个。
从更远的角度看,世界确实需要更高程度的机器学习素养。坦率地说,关于机器学习及其相关领域(如数据科学)的炒作和误信息太多了,我们需要更多理性的声音来提供机器学习能够(和不能)做什么的现实视角。
如果你愿意,加入我,一起参与这场美好的斗争,成为一名机器学习内容创作者。让我们创造一个少些机器学习伪科学、多些真实机器学习知识的世界。
从分析到实际应用:客户终身价值的案例
关于 CLV 技术和实际应用案例的全面实用指南的第一部分
·发表于 Towards Data Science ·9 分钟阅读·2023 年 7 月 2 日
--
警告:这可能是你见过的最全面的 CLV 指南,而且全部基于我在这方面的经验。所以,做好准备。(或者看看我的60 秒总结,如果你不介意错过一些精彩内容)。来源:Smarter Ecommerce。
无论你是数据科学家、营销人员还是数据领袖,可能如果你搜索过“客户终身价值”,你都会感到失望。我也有过这种感觉,当时我在电子商务领域的数据科学团队中帮助领导一个新的 CLV 研究项目。我们寻找最先进的方法,但 Google 只返回了基本的教程,配有不切实际的精心整理的数据集,以及描述 CLV 模糊和缺乏想象力的营销“空话”文章。关于将这些方法应用于实际数据和实际客户的利弊没有任何信息。我们自己学到了这些,现在我想与大家分享。
介绍:CLV 教程遗漏的所有内容。
在这篇文章中,我将介绍:
-
什么是 CLV?(我会简要介绍,因为你可能已经知道这一部分)
-
你真的需要 CLV 预测吗?还是可以从历史 CLV 计算开始?
-
你的公司从历史 CLV 信息中已经可以获得什么,特别是当你将其与其他业务数据结合时?
在系列的其余部分,我将展示:
-
CLV 预测的用途
-
计算 和 预测 CLV 的方法及其优缺点(我的 LinkedIn Learning 课程 — 营销中的机器学习 也涉及到这一点)
-
关于如何正确使用它们的经验教训。
我还会在其中融入一些数据科学的最佳实践。听起来不错吗?太好了,我们开始吧!
什么是客户生命周期价值?
客户生命周期价值是客户在零售商‘生命周期’内产生的价值:即,从第一次购买到最后一次购买之间的价值。‘价值’可以定义为纯收入:客户花费了多少。但根据我的电子商务经验,我发现更成熟的零售商往往更关注长期利润而非短期收入。因此,他们更有可能将‘价值’视为收入减去成本。不过,正如我们在第三部分中将看到的,知道需要扣除哪些成本并非易事……
计算与预测?
经验丰富的研发团队知道,对于新的数据科学项目,最好从简单的开始。对于 CLV,这可以‘简单’到使用历史交易来计算到目前为止的生命周期价值。你可以:
-
计算所有客户的简单平均值,或者
-
基于逻辑细分计算平均值,例如按人口统计组。
即使这种向后看的视角对零售商的营销和采购(即库存管理)团队也有很多用途。实际上,根据公司的数据素养水平和可用资源,这可能甚至足够(至少可以作为起点)。此外,数据科学家可以感受公司的客户的典型消费习惯,如果公司之后希望预测未来的 CLV,按客户 计算,这将是非常宝贵的。
为了帮助你和公司决定是否需要历史 CLV 见解还是未来预测,让我们查看每种情况的一些用例。毕竟,你希望营销、管理和数据科学团队从一开始就对项目的输出如何使用达成一致。这是避免构建错误内容并在之后重新开始的最佳方式。
将 CLV 信息与其他业务数据结合
许多教程仅讨论 CLV 预测 的用途,按客户 计算。他们列出了一些显而易见的用例,如‘尝试重新吸引预测中的低消费客户,让他们多购物’。但可能性远不止于此。
无论你是通过计算还是预测获得 CLV 信息,你都可以通过将其与其他数据结合来放大其商业价值。你只需每个客户 ID 提供一个 CLV 值或某种 CLV 级别评分(例如高、中、低)。然后你可以将其与其他信息来源结合,例如:
-
客户购买的产品
-
他们使用的销售渠道(店内、在线等)
-
返回信息
-
发货时间
-
等等。
我在下面做了说明。每个框显示了一个数据表及其列名。看看每个表是否包含 Customer_ID?这就是它们可以连接在一起的原因。我将在第三部分解释 CLV_Info 表的列;首先,我承诺为你提供一些用例。
可能的 CLV 数据库。来源:作者提供。
历史 CLV 计算的用途
假设你已经按总支出对所有客户进行了排名,并进行了某种细分。例如,你的市场团队要求你将数据分为支出前 10%、中间 20%和后 70%。也许你还在不同的客户子群体中进行了多次这样的操作,例如按国家划分(如果你在全球范围内有在线商店的话)。现在,假设你将这些与其他业务数据结合在一起,如上所述。你的公司可以利用这些信息做什么?
说实话,你可以从数据中提出许多问题,并对答案进行大量操作,我无法一一覆盖。我没有你拥有的领域知识,而这是数据科学中一个非常重要、却被极大低估的方面。但在接下来的几节中,我将提供一些想法,帮助你像数据驱动的营销人员一样思考。接下来的步骤由你决定……:
探索 CLV 细分和它们的需求
-
什么是顶级客户? 他们是否极为频繁且花费适中?还是他们购物的频率较低,但每次交易的花费更多?了解这些有助于你的市场营销和库存团队识别他们真正想要获取和保留的客户类型!然后,他们可以相应地规划市场营销和客户服务工作,甚至是库存和产品促销。
-
为什么你的底层客户的成本高和/或收入低? 他们是否只购买极度折扣的商品?总是退货?还是用信用购买但未及时付款?显然存在不匹配的产品—你能通过展示不同的产品来改善吗?或者这是另一个问题:你的底层客户是否总是购买一种产品,然后再也不与你购物?也许这是一个‘毒品’,应该从你的库存中移除。
-
你的高 CLV 客户是否更满意?为什么? 想象一下你是一家服装零售商,你的客户可以选择将尺码信息保存到他们的账户中。这允许你的在线商店在登录客户即将将商品添加到购物车时提供尺码建议。你还会注意到,大多数高 CLV 客户都保存了他们的尺码,并且他们的退货率较低。因此,你怀疑推荐:减少退货率 > 提高客户满意度 > 并保持顾客忠诚。
-
你如何将这些信息付诸行动?这里有一个想法:网站团队可以添加提示,提醒用户填写他们的尺码信息。理想情况下,这将增加收入,降低成本,并提高客户满意度,但如果你真的以数据为驱动,那么你需要进行 A/B 测试来验证这些变化的效果。这样你可以衡量影响,控制外部因素,并关注“保护性”指标。这些是你不希望在 A/B 测试期间发生变化的指标,例如账户删除的数量。
探索你的客户群体
上一节讲的是 CLV 层级;现在我指的是不同的客户子群体,例如按年龄段、性别或地点划分的子群体。你可以用两种方法来做这件事。
- 对你的全部客户群进行上述 CLV 分析,然后查看你的子群体如何在 CLV 层级中分布,如下所示:
不同年龄段的低(红色)、中(黄色)和高(绿色)CLV 客户的分布。来源:作者提供。
2. 首先划分为子群体,然后进行每个子群体的 CLV 分析。
或者,你可以尝试这两种方法!这取决于业务需求和可用资源。但再说一次,还有很多有趣的问题:
-
你有哪几种子群体? 忘掉我刚刚列出的那些明显的群体;让我们发挥创造力。例如,你可以按客户的原始获取渠道进行划分,或按他们现在最常用的渠道进行划分:在线与实体店,应用程序与网站。你还可以按会员等级进行划分(如果你提供的话)。利用来自网店的跟踪 Cookies,你甚至可以按偏爱的购物设备进行划分:桌面电脑、平板电脑与手机。为什么?因为可能你的移动设备购物者的购物篮价值较低,因为人们更愿意在桌面设备上进行大额购买。你能积累的领域知识越多,你的分析和——如果需要的话——机器学习工作就会越好。
-
购买行为在不同客户子群体之间有何不同? 他们什么时候购物?多频繁?花费多少?他们对促销和交叉销售的反应如何?他们的忠诚度维持多久?他们是否在生命周期的开始阶段就花费较多,随后减少,还是有其他的模式?这些信息可以帮助你规划营销活动,甚至估算未来的收入,我不需要告诉你这有多有用……
-
什么是‘典型’的客户旅程? 你是否在实体店中获得了大多数新客户?这是否意味着你的实体店很棒但你的网站糟糕?或者你的店内工作人员是否比你的网站更擅长让人们注册会员?无论如何,你可以尝试改进网站,或者至少,对你广告的渠道更加聪明。新客户优惠、新闻通讯注册折扣或朋友推荐:这些是否吸引了大量高 CLV 客户?如果没有,是时候重新评估这些活动了。
聪明地处理你的产品和市场营销
-
如果你更好地了解你的客户,你可以更好地服务他们。 对于零售商来说,这可能包括储备他们最忠实客户似乎偏爱的产品。移动电话供应商可以改善其高 CLV 客户使用的服务,比如在他们的移动应用中添加功能。当然,你还需要进行 A/B 测试任何变更,以确保不会引入客户讨厌的变化。不要放弃你的低 CLV 客户——而是尝试找出问题所在,以及如何改进。
-
同样,如果你了解你的客户,你就能用他们的语言交流。 通过在正确的时间、正确的渠道展示正确的广告,你可以获得你想要的客户,他们也愿意与你购物。
了解在客户获取上的支出
-
是否曾经想过为什么公司在你有一段时间没有在那里购物后开始给你发邮件?这是因为获得一个客户是昂贵的,他们不想失去你。这也是为什么当你浏览一个电子商务网站时,那些产品会在互联网上跟随你。那些被称为‘程序化广告’,它们出现是因为公司为那第一次点击付费,他们还不愿意放弃你。
-
作为一个零售商,你不仅仅想要将钱投入到获取任何老客户上。你希望获得并留住高价值客户:那些会忠诚且在长期内产生良好收入的客户。计算历史 CLV 可以让你计算出盈亏平衡点:每个客户‘偿还’其获取成本所需的时间。平均值是多少,哪些 CLV 等级和客户群体还清成本的速度最快?了解这些将帮助市场团队为客户获取活动制定预算,并改进他们的新客户欢迎流程(即你在新店首次购买后收到的邮件),以提高早期参与度,从而改善盈亏平衡时间。
随时间跟踪表现
-
重新评估以识别趋势。 企业和市场不断变化,超出了任何零售商的控制范围。通过定期重新计算历史客户生命周期价值(CLV),你可以不断加深对客户及其需求的理解,并了解自己是否满足了这些需求。你应该多久重新进行分析一次?这取决于你的销售和客户获取速度:例如,超市可能会比家具经销商更频繁地重新评估。它还取决于业务实际能够处理新 CLV 信息并用其做出数据驱动决策的频率。
-
重新评估以改进。 定期重新计算 CLV 将帮助你确保你获得了越来越有价值的客户。并且不要忘记在引入重大战略变化后进行额外评估,以确保你不会让数据朝着错误的方向发展。
那么 CLV 预测呢……?
我知道,我知道……你想谈谈机器学习,以及你可以如何利用 CLV 预测。但这篇文章已经够长了,所以我将这部分内容留到下次,同时还会分享我的团队在如何建模历史 CLV 和使用真实世界数据预测未来 CLV 方面的经验教训。然后在第三部分和第四部分中,我将讨论各种建模和预测方法的优缺点。
如果你对人工智能和营销感兴趣,你还可以查看我的两部分系列文章,“人工智能在营销中的作用:个性化的力量”。如果你喜欢这些内容,不要忘记订阅。下次见!
从生物学习到人工神经网络:接下来会是什么?
原文:
towardsdatascience.com/from-biological-learning-to-artificial-neural-network-whats-next-c8cf0d351af5
人工智能能否帮助我们理解大脑是如何工作的?
·发表于Towards Data Science ·10 分钟阅读·2023 年 10 月 26 日
--
在 21 世纪初,当我在 NYU Stern 学习 MBA 时,我上过一门叫做数据挖掘的课程,介绍了许多算法用于“挖掘”数据,意思是自动发现数据的意义以进行预测和决策。神经网络就是其中之一,但由于其速度较慢,需要大量数据进行训练,因此其应用案例很少。二十年后,神经网络算法在机器学习和人工智能(AI)的基础中蓬勃发展,这要归功于巨大的计算能力,这种能力去除了根本障碍,从而推动了更先进算法和模型的发明。
随着人工神经网络和深度学习的快速发展,AI 在某些领域已超越了人类。许多引人入胜的问题出现了,比如 AI 与人脑有多相似、AI 的未来目标是什么,以及 AI 可以在多大程度上取代人类智能。在本文中,我将从生物学习的神经机制和它们如何启发 AI 开始。对历史的更好理解将帮助我们把握人工神经网络与其他机器学习模型(如支持向量机、决策树、随机森林)之间的根本区别。正是受脑部学习特征的启发,人工神经网络取得了最近的突破,包括用于图像识别的卷积神经网络(CNN)和用于生成 AI 的大型语言模型(LLM)。然后,我将讨论人类智能与 AI 的区别以及我们对 AI 未来方向的看法。我们期望看到的是,AI 将继续从脑部发现中受益,同样重要的是,AI 也可以帮助我们更好地理解大脑的工作原理。思想的不断交流将推动神经科学和 AI 以健康、快速的步伐发展。
生物学习
学习是动物和人类大脑的重要特征。当一个婴儿出生时,她必须从头开始学习几乎所有的东西,包括识别面孔、说话和走路,随后是多年的学校教育和培训。学习在大脑中是如何发生的?
在 19 世纪末,西班牙神经科学家圣地亚哥·拉蒙·卡哈尔采用了一种独特的方法(戈尔基法)来染色神经元,并发现了它们特有的形状和连接模式。凭借出色的艺术天赋,卡哈尔详细描绘了许多物种主要脑区的神经解剖结构。他发现每个神经元都有一个轴突和许多类似树枝的树突。下图是他对鸽子小脑的其中一幅图画,展示了两种类型的细胞:顶部的两个大型普金耶神经元和底部的四个小颗粒细胞。每个神经元都有典型的轴突和树突。每个颗粒细胞的轴突接在普金耶神经元丰富的树突分支之一上。由于戈尔基法并不会染色大脑中的每个细胞,普金耶细胞树突的显著大小表明该细胞从小脑的颗粒细胞处接收了数百或数千个连接。
图为鸽子小脑中的普金耶细胞(A)和颗粒细胞(B)。作者:圣地亚哥·拉蒙·卡哈尔。图片来源 维基百科公共资源。
卡哈尔确认,尽管这些细胞相互连接,但它们的细胞膜并不连续。轴突末端与连接的树突之间存在一个微小的间隙,称为突触。他还绘制了一些关于动物大脑复杂神经连接的非凡图画。下面是海马体的一个例子,今天我们知道它负责短期学习和记忆的大脑区域。我们可以看到,细胞体排成层状。一个轴突在层之间穿行,同时与多个树突和细胞体交叉。这幅图是对大脑中生物神经网络的最早准确描绘之一。他对大脑神经结构的原始研究使他成为现代神经科学的奠基人。卡哈尔和戈尔基于 1906 年获得了生理学和医学诺贝尔奖。
哺乳动物海马体神经电路的绘图。作者:圣地亚哥·拉蒙·卡哈尔。图片来源 维基百科公用资源
神经通信需要细胞内的动作电位发放和突触中释放的神经递质。动作电位,也称为神经冲动或尖峰,发生在神经元细胞膜的电压迅速升高和降低时。这种去极化导致相邻的膜位置也类似去极化,从而在毫秒内沿轴突快速传播。当尖峰到达轴突末端时,突触释放神经递质,这些小分子在大脑中充当信使,跨神经元进行通信。当突触后神经元通过其多个树突接收到足够的神经递质时,它会发放电信号,这些电信号然后通过突触以相同的方式沿其轴突向下一个连接的细胞传递。
图片来源: 维基百科公用资源
单个神经元本身就是一个信息处理系统。树突在接受和传递来自多个来源的信息中发挥作用,而细胞体则整合信号并通过轴突将其传递给下一个神经元。在这一层面上,有许多变量:
-
神经元在接收到来自树突的信息时并不总是会发放信号,而仅在接收到的信号达到阈值时才会发放。
-
轴突可以在同一大脑区域内的层之间传递信息,也可以在神经系统中长距离(例如超过 1 米)传递到另一个区域。
-
动作电位的频率越高,轴突突触释放的神经递质就越多,突触后细胞发放信号的可能性也就越大。
然而,正如我们身体中的其他细胞一样,神经元非常微小(其细胞体直径为 4–100 微米),单个神经元的处理能力有限。神经元的另一个重要作用是进行网络内的通信。传输的信息量不仅与单个细胞的放电频率有关,还与网络中同时放电的细胞数量有关。
1949 年,心理学家 Donal O. Hebb 在他的书《行为的组织:一种神经心理学理论》中提出了著名的赫布规则:
当细胞 A 的轴突足够接近以激活细胞 B,并且重复或持续参与激发它时,会在一个或两个细胞中发生某种生长过程或代谢变化,使得 A 作为激发 B 的细胞之一的效率得以提高。
用简化的术语来说,赫布理论预测那些同时激活的细胞具有更强的生物连接,这解释了大脑中的联想学习。1966 年,挪威生理学家 Terje Lømo 发现了“长期增强”现象,证明了赫布的理论是正确的。Lømo 刺激了输入神经纤维,并记录了麻醉兔海马体中突触后细胞的动作电位。在通过单脉冲刺激收集基线后,他施加了一组高频刺激,并观察到增强的反应,这些反应可以持续超过 10 小时。这是首次展示近期神经活动的历史可以改变脑细胞之间连接强度的现象。此后,学习和记忆的神经科学蓬勃发展。科学家们使用各种动物模型研究不同脑区的长期增强现象,从行为到细胞和分子水平。神经科学家 Eric Kandel 开发了一个简单的海虹虫动物模型。他和他的实验室彻底研究了海虹虫学习行为相关的各种长期增强现象后,突触中的电学、化学和分子变化。凭借其终身成就,Eric Kandel 在 2000 年获得了诺贝尔奖。
今天在其他神经科学学科的进展也表明了不同类型的神经可塑性,可能涉及细胞生长或新细胞生成。换句话说,赫布规则可能是大脑学习机制中的众多机制之一。神经科学在识别和理解其他学习机制方面仍有很长的路要走。
人工神经网络
1943 年,神经生理学家 Warren McCulloch 和逻辑学家 Alter Pitts 共同提出了第一个数学神经网络模型,该模型由人工神经元组成,每个神经元具有一个或多个二进制输入(模拟树突)和一个二进制输出(模拟轴突)。这些互连的人工神经元网络能够计算任何逻辑规则。然而,该模型没有学习的功能。
1958 年,心理学家弗兰克·罗森布拉特发明了第一个人工神经网络(ANN),称为感知器。它有两个层次的人工神经元:输入层和输出层。弗兰克显著地采用了赫布规则,使得网络在训练过程中调整连接权重,直到结果符合训练数据中的预期。这一特性使得神经网络可以在没有预定义规则的情况下学习任何内容,仅依赖于训练数据中提供的示例结果。这是 ANN 历史上的一个革命性时刻,因为这是计算机首次能够像生物大脑一样在没有特定规则或指令的情况下解决问题。
随后发生了两个突破,使得人工神经网络真正腾飞。其一是在输入层和输出层之间添加一个或多个内部隐藏层,使深度学习算法能够逐步从原始输入中提取更高层次的特征。其次,通过使用反向传播技术显著优化了权重调整。隐藏层也有生物学上的对应物。例如,新皮质有六个细胞层。海马体有三个层次,如原始的卡哈尔图所示。凭借现代基础设施和大数据的强大计算能力,人工神经网络与深度学习已经大规模扩展,增加了更多隐藏层和可调连接,以解决更复杂的任务。
生物神经网络与人工神经网络
随着人工智能利用人工神经网络和深度学习的快速进展,关于人工智能的未来及其与人类之间的有趣关系提出了许多问题。考虑到我们上面讨论的生物神经网络和人工神经网络之间的相似性,让我们深入探讨两者之间的差异。
- 我们仍然对大脑的工作原理知之甚少
随着过去十年中人工智能取得的巨大进步,我们现在进入了一个人工智能也可能激发神经科学家对大脑内部工作原理的研究时代。由于大脑是思维和驱动行为的器官,在清醒、活动的动物和人类身上研究它是极其困难的。人工神经网络使科学家能够假设、建模并设计未来的实验以进行神经科学研究。此外,人工智能研究人员和神经科学家可以通过整合来自大脑的细胞甚至分子发现,合作模拟神经网络在计算机上的新兴行为。
换句话说,人工智能不仅从神经生物学中获取灵感,神经科学家也可以从人工智能的进展中获得灵感。例如,反向传播是一个数学模型,它极大地提升了人工神经网络(ANN)的性能,达到了或超过了人类水平。然而,目前没有证据表明这种模型在大脑中存在,神经科学家和人工智能研究人员都在积极寻找其生物学版本。
2. 人工智能由人类管理
尽管人工智能可以在没有工程师控制的规则或方程式的情况下自我学习,但人工智能模型高度依赖输入数据的质量,特别是训练数据。工程师和数据科学家还必须深入了解数据,以使训练有效且结果可信。鉴于此,这些模型是被动的,依赖于人类策划的训练。人工智能系统被设计为在特定任务上比人类更高效或准确,特别是在视觉、预测分析、机器人技术、自然语言处理(NLP)和国家语言理解(NLU)方面。
相比之下,人类可以在数据更少且质量较差的情况下学习几乎任何东西。他们通过与环境互动来获取信息,并善于利用上下文。此外,人类可以在未定义的概念空间中无缝推理,并且在收集足够信息在特定时间内不可能的情况下,善于处理不确定性。
3. 人类智能和人工智能在根本上不同
人类智能依赖于生物构造,并受到数百万年的进化和数千年的文化的塑造。它复杂且多面,涵盖了许多认知和情感能力。人类大脑似乎是一个高度集成的系统,能够互动地收集信息、带着感情做决策、使用语言进行交流并同时采取行动。另一方面,我们都受到似乎不可避免的认知偏差和不可靠记忆的困扰,这似乎是卓越进化设计中的“缺陷”。
相反,人工智能是基于软件构建并在计算机硬件上执行的,具备执行通常需要人类智能的任务的能力,如学习、模式识别、问题解决和决策制定。尽管人脑激发了基本原则,但工程师和数据科学家使用算法和数学模型实现了人工神经网络。鉴于此,人工智能提供了具体、详细、可重复和可扩展的结果,这些是人类难以实现的。这些恰恰是自动化和提高人类努力效率的特点,并补充了人类的限制。
结论
正如鸟类启发我们建造飞机飞行一样,人工神经网络因受脑部神经结构的启发而取得成功。由于基础材料本质上不同,我们从不期望人工智能直接复制人脑;相反,我们期待它成为另一种类型的智能,具有自身的设计和优势,能够帮助人类解决特定问题。
此外,大脑是一个高度复杂的生物系统。研究人类行为和认知的神经机制的实验方法相对有限。AI 可以成为一个强大的工具,提供想法和支持证据,以理解大脑的内部运作。我们在未来十年中应该会看到这种相互启发,从而推动两个领域的共同发展。
遗传算法简介
原文:
towardsdatascience.com/from-biology-to-computing-an-introduction-to-genetic-algorithms-b39476743483
探索进化计算的力量及其在日常问题中的应用
·发表于数据科学前沿 ·阅读时间 6 分钟·2023 年 4 月 17 日
--
背景
查尔斯·达尔文在 19 世纪发展他的进化论时,几乎未曾想到这一理论会对计算算法产生深远的影响。
自然选择、适者生存、突变和交叉等概念都导致了‘最佳’变体的存活,并将‘最佳’特征传递下去。约翰·亨利·霍兰将这些概念发展成为元启发式遗传算法,于 1975 年提出以解决数学优化问题。 生物过程对机器学习的影响并非首次出现。最著名的例子或许是 1958 年弗兰克·罗森布拉特受到大脑中真实神经元启发开发的神经网络感知机。
大多数遗传算法用于解决组合优化问题,在这些问题中,由于排列组合的庞大规模,暴力搜索是不可行的。示例包括背包问题和旅行推销员问题。
如果你是一名数据科学家,你可能会在职业生涯中遇到类似的问题,因此了解如何处理这些问题是很有价值的,本文将介绍其中一种最佳且最常见的方法。我们将深入探讨遗传算法的理论、方法论和一般用途,以展示如何实现它们来解决几乎所有的优化问题。
符号与术语
遗传算法的术语大多源自其相应的生物过程:
-
编码与解码: 将问题映射到算法可以利用的方便的数值格式。常见的技术包括 二进制编码和排列编码。
-
种群: 一组可能的解决方案。
-
染色体: 一个单一的可能解决方案。
-
基因: 染色体中一个元素的位置表示。
-
等位基因: 基因的值。
-
适应度函数: 为每个解决方案赋予一个分数,以确定其对优化问题的‘适应度’。
这些不同的术语可以通过下面的图示进行说明:
图表由作者提供。
算法
初始化
算法开始时生成问题的初始种群。这需要在可能的搜索空间中具有足够的多样性,理想情况下总数在100或1000的范围内。
选择
在确定了我们的种群之后,我们需要选择解决方案(父代)以便进行下一代的繁殖。父代的选择是基于它们的适应度,以确保‘最佳’基因得以传递。
一些技术用于选择:
-
精英主义: 选择当前种群中适应度函数最佳的若干个解决方案。
-
轮盘赌选择:从当前种群中的解决方案(染色体)的概率分布中进行抽样,每个解决方案的概率 p_s 与其适应度得分 f_s 成正比:
方程由作者在 LaTeX 中生成。
- 锦标赛选择:: 这涉及随机选择解决方案并进行锦标赛,赢家是适应度得分最好的那个。
选择过程还有许多其他方法,以及上述技术的不同变体。你也可以结合选择程序来生成混合方法。没有‘一刀切’的方法,最好尝试各种类型。
交叉
现在我们有了父代,我们需要结合它们的遗传材料以产生后代,从而生成一个新的、更适应的种群。与选择阶段类似,有各种操作符来执行交叉,这很依赖于问题的编码类型。
让我们了解一些最常见的:
- 单点交叉: 在父代染色体的选定点之间交换基因:
图示由作者提供。
- 双点交叉: 在父代染色体的两个选定点之间交换基因:
图示由作者提供。
- 均匀交叉: 每个基因从两个父代的相应基因中随机选择:
图示由作者提供。
这些技术适用于二进制编码的染色体,对于其他编码类型,可能需要不同的操作符。链接的这里有一篇很好的文章,给出了更多复杂的交叉操作符的综合列表。
变异
算法的最后一步是变异。这类似于生物变异,其中后代的 DNA 可能会发生随机改变。变异是对解中的基因进行小概率的轻微修改。如果概率很高,那么遗传算法就变成了随机搜索。变异操作符的主要动机是使种群多样化,减少局部最优的可能性。
图示由作者提供。
让我们了解一些简单易用的变异:
位翻转: 对染色体中的基因进行迭代,每次通过时以小概率随机翻转一个位:
图示由作者提供。
- 交换: 在染色体中选择两个基因,以低概率交换它们:
图示由作者提供。
链接的这里是一个进一步的变异操作符列表。
与交叉类似,上述变异操作符适用于二进制编码问题,因此可能不适合其他类型的问题和编码。
终止
算法可以因多种原因结束,这些原因由用户自行决定:
-
达到一定代数
-
达到了期望的适应度
-
计算资源耗尽
-
适应度评分已经达到平台期
应用
遗传算法用于多种领域,包括但不限于:
-
生产计划中的调度
-
车辆路线问题
-
金融市场与投资组合优化
-
训练神经网络
-
图像处理
弱点
遗传算法的一些漏洞包括:
-
可扩展性不如其他优化算法,因此可能有较长的计算时间
-
难以充分调整超参数,如变异和选择,可能导致不收敛或无效结果
-
并不能保证找到全局最优解,不过这是所有元启发式方法的权衡
-
一个昂贵且复杂的适应度函数也可能导致长时间的计算
总结与思考
遗传算法的名称来源于进化生物学中的类似过程。它是一种元启发式优化算法,从初始种群开始,通过选择、交叉和变异操作迭代使用种群中的最佳解来创建新的、更好的解(后代)。它可以用于许多领域,如医学和供应链,但根据评估指标和选择的超参数,它可能会面临较长的计算时间。
另一个事项!
我有一个免费的通讯,Dishing the Data,在其中我分享成为更优秀数据科学家的每周技巧。没有“浮夸”或“点击诱饵”,只有来自实践中的数据科学家的纯粹可操作的见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读《Dishing The Data》,由 Egor Howell 编写的 Substack 出版物……
联系我!
参考文献与进一步阅读
-
优化算法。 Mykel J. Kochenderfer 和 Tim A. Wheeler。2019*。
-
组合优化:理论与算法。 Bernhard Korte 和 Jens Vygen。2018。
-
Holland, J.H. (1975) 《自然与人工系统中的适应》。密歇根大学出版社,安娜堡。(第二版,麻省理工学院出版社,1992 年。)
-
Rosenblatt, F. (1958). 识别机:一种用于信息存储和组织的概率模型。《心理学评论,65(6),386–408.
doi.org/10.1037/h0042519
从商业学生到 Spotify 的数据科学家
原文:
towardsdatascience.com/from-business-student-to-data-scientist-in-tech-eb7bbaab1784
数据科学家在科技行业的初年历程
从对数学感到害怕到成为著名科技公司全职数据科学家的时间线
·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 7 月 3 日
--
在我经常收到的 LinkedIn 上最常见的问题中,有一个问题始终突出:
我如何一夜之间从商业转到工程,并成为数据科学家的?
在这个故事中,我将深入讲述我的个人历程,并与大家分享我所遵循的步骤、面临的挑战和获得的宝贵经验,这些都推动我直接成为了科技行业的数据科学家。
图片来源:Carolyn Christine 在 Unsplash
成为数据科学家没有一种特定的方式。正如俗话所说,条条大路通罗马。然而,我在这里分享 其中 一种方法,特别是对于那些从商业学位背景开始并且几乎没有科学背景的人。
在就业市场上,前景模糊,知道你想成为哪种类型的数据科学家将影响你需要具备的技能。
你在外面常见的数据科学家的最常见原型有:
1. 数据/决策科学家: 利用数据生成洞察和价值,以推动决策。通常需要了解机器学习。
2. 研究数据科学家: 作为产品开发机器学习模型。强大的数学背景是高度要求的。这通常是科技领域博士们的角色(但不限于此)。
3. 数据科学家/机器学习工程师: 这是一个处于决策和研究数据科学家交汇处的角色。
在我的文章中,我将主要关注我成为科技领域数据/决策科学家的旅程,在我的经验中,这似乎是最主要的数据科学家类型。
为什么
在深入探讨 如何 之前,我们首先需要奠定 为什么 的基础。相信我,如果你从一开始就没有把 为什么 理清楚,你不会走得太远。
成为数据科学家的旅程是充满挑战的,但无疑也是最值得的之一。事实上,数据科学家今天有着最酷、最令人羡慕的工作之一的理由清单很长。现在,我将重点关注在 我 的清单上排名第一的那个。
对于任何问我 为什么选择数据科学? 的人,无论是在面试中还是好奇的朋友,我的回答总是一样的。我想成为一名侦探,所以我决定成为一名数据科学家。
你可能会想这两者之间有什么关系?
我遇到的第一批数据科学家立刻让我觉得他们体现了现代福尔摩斯的形象。
数据科学家每天通过知识来解决难题。最终,他们花费大部分时间在调查数字,以找到只有敏锐的分析思维才能解决的复杂问题。这正是侦探所做的。
有时我对自己的想法 — 照片由 Alexey Turenkov 在 Unsplash 上拍摄
我一直感到对 福尔摩斯 冒险故事的深切兴奋。他不顾一切地追踪线索,直到谜团被揭开。我想成为像他一样的冒险家。但我几乎没想到自己会放弃学业去与警察一起破案。我想这对我来说有点过于极端,我有点希望不要在生命早期就与坏人对抗。所以,成为数据科学家感觉像是两全其美。
只是需要一些时间来触发那个顿悟。到那时,成为数据科学家感觉与我当时追求的商业学位有很大的距离。
回顾过去,我与福尔摩斯唯一的共同点就是我对看似不可能的任务的执着。这些任务非常脱离现实,考虑到我对编程几乎没有任何经验。但我没想到的是,接受这种跳跃的信念竟然成了我追求生命中最大激情——音乐的金钥匙。
图片来源:作者(Midjourney)
如何
第 1 步 — 面对并接受数学怪物
尽管我对科学有着长期的迷恋,但上帝还没有认为我配得上轻松解读科学术语的高度独特的礼物。我也没有天赋音准,但这与此无关。
所以,不管怎样,毕业后,我自然地趋向于这个世界上大多数对科学科目一无所知的高中生所选择的——商业学科。
我与数学的关系一直颇为波折。我在高中时主修经济学。物理让我感觉像是在平行宇宙中,数学概念听起来像是外星文明的秘密代码。课程需要时间才能理解,但… 它们迟早会理解。有时,可能会晚得多。
图片来源:作者(Midjourney)
我很早就知道,数学在我的包袱中将会派上用场。因此,我不会让我的科学不足阻止我超越自己的能力去追求梦想。
在高中最后一年,我决定彻底面对数学怪兽。我把所有精力投入到驯服这个怪兽上,直到它最终屈服。
这是一个改变游戏规则的时刻。
这给了我必要的信心,后来在大学期间可以同时主修商业和辅修数学。
在我追求的所有数学课程中,这些是为解决数据科学和机器学习问题奠定基础的基本课程:
-
微积分
-
线性代数
-
统计与概率论
对于所有数学恐惧者来说,请记住,我们之所以讨厌某件事,往往是因为我们在其中表现不佳。我们越早提高和精通一门学科,我们的看法就会改变得越多。
所以,如果你的目标是成为一名厉害的数据科学家,是时候正面迎接数学怪兽,告诉他谁才是老板了!
商业显然不是我的真正召唤。我太享受被数学怪兽的诱惑折磨了。所以在我最后一个学期,我深入探索了与数学相关的职业道路。
最终,我的搜索将我引向了数据科学领域。我抓住了一个作为数据分析师实习的机会,同时进入了 NYU 的城市信息学硕士课程(这是智能城市领域应用数据科学的花哨说法)。
这是漫长系列中的第一个里程碑
第 2 步——建立对编程的直觉需要时间
你可能会想,一个没有任何编程经验的商业毕业生是如何成功进入工程学校的?嗯,还记得我在本科时上的那些数学课程吗?
事实上,你开始进行机器学习(ML)不需要超出这三门课程的知识。
NYU 的学位是一个沉浸式但紧张的 12 个月课程,我从一开始就直接投入到使用 Python 进行 ML 建模、用 SQL 构建数据库和在 Spark 上处理大数据(或者至少尝试)中。
我必须在这里坦白。从商业到工程的突然转变是一段忙碌的经历。那一年几乎感觉像是一场濒临死亡的经历。
第一次学习多种编程语言意味着要培养对每一种语言的不同直觉,这需要时间。在一个紧凑的时间框架内同时掌握所有这些语言,并承担高昂的学费(还有门口的疫情),这并不是我会建议胆小者尝试的。
对于一个在此之前从未写过一行代码的人来说,这感觉就像是对我的大脑造成了巨大的冲击波,完全超负荷。可怜的大脑不知道我们是如何从管理原则一下子转到全力运行机器学习模型的。
事后看来,我会做得不同的一件事是:在深入机器学习之前就学习编码,而不是同时进行。
作者提供的图片(Midjourney)
玩弄机器学习很有趣,但远远不够。我仍未完全理解那些 Python 包背后的运作机制。
除非你能够深入挖掘,否则你永远不会被视为科技行业的真正数据科学家。 你必须熟练解释那些预打包的机器学习算法背后的基本机制。
所以我知道我必须理解这些机制,但从那么远的地方过来,一年时间能达到的也有限。
那时候,COVID-19 爆发了,美国的工作机会就像我之前的编码技能一样几乎不存在。所以我想,我可以在学术界多待一年(或者两年)。我申请并被录取到法国的一个独特双学位项目,该项目结合了数据科学的两个最佳领域:商业与机器学习。
第三步——从库导入器到解释者:释放内在的真正数据科学家
追求这个学位结果证明是我一生中最好的决定之一。它让我在法国顶尖的商业与工程学院学习,这曾经是不可想象的成就。
我在学校从未是最聪明的,但我总是有一种在最艰难的道路上找到出路的天赋。决心和固执是我最重要的资产,所以我早早地将它们发挥到极致。
在这两年转型的过程中,我学到了许多自己甚至不知道需要学习的数据科学知识。我在这过程中获得的武器至今仍在指导我,我在这里与你分享:
-
理解机器学习算法背后的数学将专业人士与 Python 库用户区分开来。 我学会了如何展示数学证明,但我相信掌握理论背后的关键概念就足够了。无需直接处理方程式本身。
-
精通 Python 和 SQL 是“数据科学技术”入门包中的基本技能。 提取和构建数据管道需要停留在像 BigQuery 这样的数据仓库上,而这些数据仓库通常由 SQL 支持。同时,掌握 Python 基础将帮助你进行数据准备和分析。
-
你需要学习线性代数和微积分来帮助你理解机器学习理论的基础,但没有什么能比得上将统计学和概率论概念纳入你的工具箱。 统计学在数据科学家的日常工作中有直接应用,因此最好尽早掌握统计显著性和概率分布的基础知识。
-
如果你不能将数据洞察传达给非技术人员,那它们将毫无价值。 提升你的讲故事技巧是数据科学家的持续旅程,因此最好尽早上船。
-
培养在与聪明才智和经验丰富的同行共事时的韧性和耐心。 冒名顶替综合症很容易出现,特别是当你来自不同背景时。自我怀疑是个讨厌的家伙,所以最好尽早熟悉它并摆脱它。
从很远的地方开始,我花了两年的训练才敢于称自己为数据科学家。
我的个人故事并不是成为数据科学家的绝对指南,因为每个人的旅程都是独特的。相反,它是实现这一目标的各种方式之一的见证。我希望我的经验能够激励你,引导你在自己的冒险中前行。
通过接受这些经验教训,我作为数据科学家的水平提升了。它们为我提供了启动科技职业所需的工具。
最优秀的程序员不一定是最优秀的数据科学家。要在技术领域中作为数据科学家蓬勃发展,来自商业背景的我学到了一些必须具备的条件:
-
接受数学的不适。 掌握线性代数、微积分和统计学与概率论的基础知识。
-
花时间彻底学习 Python 和 SQL。 避免让自己被过多的编程框架压垮。
-
深入研究机器学习的数学基础,以揭示预包装算法的奥秘。 准备好解释它们的机制。
-
从一开始就磨练你的讲故事技巧。 掌握以引人入胜和易于理解的方式传达复杂概念的艺术。
-
将商业概念与之前获得的知识相结合,创造出强大的组合。
过去,我低估了自己商业学士学位的价值。它常常让我觉得自己花了多年时间追寻错误的道路。
但在加入 Spotify 后,我发现商业与数据科学专长的融合创造了最优秀的数据科学家。真正的力量在于那些能够无缝导航两个领域的人。你完全有理由成为其中之一!
成为紫色的体验真是刺激无比
我有礼物要送给你🎁!
订阅我的新闻通讯 K’s DataLadder,你将自动获得我的终极 SQL 备忘单,其中包含我在大科技公司工作中每天使用的所有查询,以及另一个秘密礼物!
我每周分享作为技术数据科学家的经历,以及实用的技巧、技能和故事,旨在帮助你提升自我——因为没有人真正知道,直到他们亲身经历!
如果你还没有做这件事
再见啦!
从因果树到森林
原文:
towardsdatascience.com/from-causal-trees-to-forests-43c4536f1481
因果数据科学
如何使用随机森林进行政策定位
·发表于 Towards Data Science ·13 分钟阅读·2023 年 2 月 20 日
--
封面,图片由作者提供
在我之前的博客文章中,我们已经探讨了如何使用因果树来估计政策的异质性处理效应。如果你还没有阅读,我建议先阅读那篇文章,因为我们将以那篇文章的内容为基础,从那里开始。
为什么要关注异质性处理效应(HTE)?首先,异质性处理效应的估计使我们能够选择哪些用户(患者、用户、客户等)接受治疗(药物、广告、产品等),这取决于他们预期的结果(疾病、公司收入、客户满意度等)。换句话说,估计 HTE 使我们能够进行定位。实际上,正如我们在文章后面将看到的,一个治疗在平均情况下可能无效或甚至适得其反,但对某一子集的用户却带来积极的效果。相反的情况也可能成立:一种药物在平均情况下有效,但如果我们识别出有副作用的用户,其效果可能会得到改善。
在这篇文章中,我们将深入探讨因果树的扩展:因果森林。正如随机森林通过平均多个自助抽样的树来扩展回归树一样,因果森林扩展了因果树。主要的不同之处在于推断的角度,这并不那么直接。我们还将了解如何比较不同的 HTE 估计算法的输出,并如何将它们用于政策定位。
在线折扣
在文章的其余部分,我们继续使用因果树文章中的玩具示例:我们假设我们是一个在线商店,并且我们希望了解是否向新客户提供折扣会增加他们在商店中的支出。
图片,由作者使用NightCafé生成
为了了解折扣是否具有成本效益,我们进行了一项随机实验或A/B 测试:每当新客户浏览我们的在线商店时,我们会随机分配其到一个处理条件。对于处理用户,我们提供折扣;对于对照用户,则不提供。我从[src.dgp](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/dgp.py)
导入数据生成过程dgp_online_discounts()
。我还从[src.utils](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/utils.py)
导入了一些绘图函数和库。为了包含代码、数据和表格,我使用了Deepnote,这是一个类似 Jupyter 的基于网页的协作笔记本环境。
我们有关于 100,000 名在线商店访客的数据,我们观察他们访问网站的时间
、使用的设备
、他们的浏览器
和他们的地理区域
。我们还看到他们是否获得了折扣
(我们的处理)以及他们的支出
(我们关注的结果)。
由于处理是随机分配的,我们可以使用简单的均值差异估计量来估计处理效果。我们预计处理组和对照组在除折扣
之外是相似的,因此我们可以将支出
中的任何差异归因于折扣
。
折扣似乎是有效的:在处理组中,平均支出增加了 1.95 美元。但是所有客户的影响是否相同?
为了回答这个问题,我们希望估计异质性处理效果,可能是在个体层面上。
因果森林
计算异质性处理效果有很多不同的选项。最简单的一种方法是将关注的结果与异质性维度交互。这个方法的问题在于选择哪个变量。有时我们有先验信息可以指导我们的行动;例如,我们可能知道移动
用户的平均支出高于桌面
用户。其他时候,我们可能出于商业原因对某个维度感兴趣;例如,我们可能希望在某个区域
投资更多。然而,当我们没有额外信息时,我们希望这个过程是数据驱动的。
在上一篇文章中,我们探讨了一种数据驱动的方法来估计异质治疗效果:因果树。我们将扩展到因果森林。然而,在开始之前,我们必须介绍一下它的非因果兄弟:随机森林。
图片,由作者使用NightCafé生成
随机森林,顾名思义,是回归树的扩展,在其基础上增加了两个独立的随机性来源。具体而言,随机森林算法利用许多不同回归树的预测结果,每棵回归树都是在数据的自助样本上训练的,并将这些预测结果进行平均。这一过程通常被称为袋装法,自助聚合,并可以应用于任何预测算法,并非随机森林所特有。额外的随机性来源来自于特征选择,因为在每次分裂时,只考虑所有特征X的随机子集进行最优分裂。
这两个额外的随机性来源极为重要,有助于随机森林的优越性能。首先,袋装法使随机森林通过对多个离散预测进行平均,产生更平滑的预测。随机特征选择则允许随机森林更深入地探索特征空间,使其能发现比简单回归树更多的交互作用。事实上,变量之间可能存在单独来看预测能力不强(因此不会产生分裂),但一起来看却非常强大的交互作用。
因果森林是随机森林的等效方法,但用于估计异质治疗效果,正如因果树和回归树一样。与因果树一样,我们面临一个基本问题:我们感兴趣的是预测一个我们无法观察到的对象:个体治疗效果τᵢ。解决方案是创建一个辅助结果变量*Y**,其对每个观测值的期望值恰好就是治疗效果。
辅助结果变量,由作者提供的图片
如果你想了解更多关于为什么这个变量对个体治疗效果是无偏的细节,可以查看我的上一篇文章,在其中我会详细讲解。简而言之,你可以将*Yᵢ**解释为单个观测值的均值差异估计量。
一旦我们有了结果变量,还有几件事需要做,以便使用随机森林来估计异质性处理效果。首先,我们需要构建在每个叶子中有相同数量的处理和对照单位的树。其次,我们需要使用不同的样本来构建和评估树,即计算每个叶子的平均结果。这个过程通常被称为诚实树,它对于推断非常有帮助,因为我们可以将每个叶子的样本视为与树结构独立。
在进入估计之前,我们首先为分类变量device
、browser
和region
生成虚拟变量。
我们现在可以使用随机森林算法来估计异质性处理效果。幸运的是,我们不需要手动完成所有这些操作,微软的EconML包中有一个很好的因果树和森林实现。我们将使用CausalForestDML
函数。
与因果树不同,因果森林更难解释,因为我们不能可视化每一棵树。我们可以使用SingleTreeCateInterpreter
函数来绘制因果森林算法的等效表示。
因果森林模型表示,作者提供的图像
我们可以像解释因果树模型一样解释树状图。在顶部,我们可以看到数据中的平均\(Y^*\),1.917\(。从那里开始,数据根据每个节点顶部突出显示的规则被分成不同的分支。例如,第一个节点根据`time`是否晚于 11.295\)将数据分成两个组,分别为 46,878\(和 53,122\)。在底部,我们有最终的分区及预测值。例如,最左侧的叶子包含 40,191\(的观察,其中`time`早于 11.295\)且browser
非 Safari,我们预测支出为 0.264$。节点颜色越深表示预测值越高。
这个表示的问题在于,与因果树的情况不同,它只是模型的一个解释。由于因果森林由许多自助抽样树组成,因此无法直接检查每棵决策树。理解哪个特征在决定树的分裂时最重要的一种方法是所谓的特征重要性。
显然,time
是异质性的第一个维度,其次是device
(特别是移动设备)和browser
(特别是 Safari)。其他维度不太重要。
现在让我们检查模型性能。
性能
通常,我们无法直接评估模型性能,因为与标准机器学习设置不同,我们无法观察到真实情况。因此,我们不能使用测试集来计算模型的准确性度量。然而,在我们的案例中,我们控制数据生成过程,因此可以访问真实情况。让我们从分析模型如何沿数据的分类 维度(如device
、browser
和region
)估计异质处理效果开始。
对于每个分类变量,我们绘制了实际和估计的平均处理效果。
每个分类值的真实和估计的处理效果,图片由作者提供
因果森林算法在预测与分类变量相关的处理效果方面非常有效。至于因果树,由于算法具有非常离散的特性,这种情况是预期中的。然而,与因果树不同的是,预测结果更为细致。
我们现在可以进行更相关的测试:算法在处理连续变量如time
时表现如何?首先,让我们再次隔离time
上的预测处理效果,忽略其他协变量。
现在我们可以复制之前的图,但应用于time
维度。我们绘制了每天每个时段的真实和估计的处理效果的平均值。
随时间维度的真实和估计的处理效果,图片由作者提供
我们现在可以完全理解因果树和森林之间的差异:在因果树的情况下,估计结果基本上是非常粗略的阶跃函数,而我们现在可以看到因果森林如何产生更平滑的估计。
我们现在已经探索了模型,是时候使用它了!
政策目标定位
假设我们考虑向访问我们在线商店的新客户提供 4$的折扣。
折扣对哪些客户有效?我们估计了 1.9492$的平均处理效果,这意味着折扣在平均情况下并不真的有利可图。然而,我们现在能够针对单个个体,只对来访客户的子集提供折扣。我们将探讨如何进行政策目标定位,并为了更好地理解定位的质量,我们将使用因果树模型作为参考点。
我们使用相同的CausalForestDML
函数构建一个因果树,但将估算器数量和森林大小限制为 1。
接下来,我们将数据集拆分为训练集和测试集。这个想法与交叉验证非常相似:我们使用训练集来训练模型——在我们的例子中是用于异质处理效应的估计器——并使用测试集来评估其质量。主要区别在于我们无法在测试数据集中观察到真实结果。但我们仍然可以使用训练-测试拆分来比较样本内预测与样本外预测。
我们将 80%的所有观察值放入训练集中,将 20%放入测试集中。
首先,让我们只在训练样本上重新训练模型。
现在我们可以决定一个目标政策,即决定向哪些客户提供折扣。答案似乎很简单:我们向所有我们预计治疗效应大于成本 4 美元的客户提供折扣。
一个允许我们理解处理效果及其方式的可视化工具是所谓的处理操作特征(TOC)曲线。这个名字让人联想到更著名的接收者操作特征(ROC)曲线,该曲线绘制了不同阈值下的真正例率与假正例率。其想法类似:我们绘制不同处理人口份额下的平均处理效应。在一个极端情况下,当所有客户都被处理时,曲线取值等于平均处理效应,而在另一个极端情况下,当仅一个客户被处理时,曲线取值等于最大处理效应。
现在让我们计算曲线。
现在我们可以绘制两个 CATE 估计器的处理操作特征曲线。
处理操作特征曲线,图像由作者提供
正如预期的那样,由于我们增加了处理客户的比例,TOC 曲线对于两个估计器都是递减的。换句话说,我们在释放折扣时越有选择性,每个客户的优惠效果越高。我还绘制了一条与折扣成本水平相同的水平线,以便我们可以将 TOC 曲线下方且成本线以上的阴影区域解释为预期利润。
这两个算法预测的处理份额相似,约为 20%,其中因果森林算法针对的客户稍多。然而,它们预测的利润却大相径庭。因果树算法预测了一个小且恒定的利润差距,而因果森林算法预测了一个更大且更陡的利润差距。哪个算法更准确?
为了比较这两者,我们可以在测试集上进行评估。我们使用在训练集上训练的模型来预测处理效应,并与在测试集上训练的模型的预测结果进行比较。请注意,与机器学习标准测试程序不同的是,在我们的案例中,我们无法将预测结果与真实值进行评估,因为处理效应没有被观测到。我们只能相互比较两个预测结果。
看起来因果树模型比因果森林模型表现更好,总净效应为 8,386$$,而因果森林为 4,948$$。从图中我们还可以理解差异的来源。因果森林算法趋向于更为严格,并且处理的客户较少,虽然没有假阳性,但也有很多假阴性。另一方面,因果树算法则更为宽松,将折扣
分发给更多的新客户。这导致了更多的真正阳性,但也有假阳性。净效应似乎偏向于因果树算法。
通常情况下,我们会在这里停止,因为我们不能做更多的事情。然而,在我们的案例中,我们可以访问真实数据生成过程。因此,我们可以检查这两种算法的真实准确性。
首先,让我们从处理效应的预测误差方面进行比较。对于每种算法,我们计算处理效应的均方误差。
随机森林模型更好地预测了平均处理效应,其均方误差为 0.5555\(,而不是 0.9035\)。
这是否意味着更好的目标定位?我们现在可以重复之前的条形图,以了解这两种算法在政策定位方面的表现。
图形非常相似,但结果差异很大。实际上,因果森林算法现在的总效应为 10,395\(,而因果树算法为 8,828\)。为何会有这种突如其来的差异?
为了更好地理解差异的来源,让我们根据真实情况绘制 TOC 图。
处理操作特征曲线,图片由作者提供
正如我们所见,TOC 非常偏斜,并且存在一些具有非常高平均处理效应的客户。随机森林算法更能识别这些客户,因此总体上更有效,尽管其目标客户较少。
结论
在这篇文章中,我们看到了一种用于估计异质性处理效应的非常强大的算法:因果森林。因果森林建立在因果树的相同原理基础上,但受益于对参数空间的更深入探索和袋装方法。
我们还看到了如何利用异质治疗效应的估计来进行政策定位。通过识别具有最高治疗效应的用户,我们能够使本来无法获利的政策变得有利可图。我们还看到,政策定位的目标可能与异质治疗效应估计的目标不同,因为分布的尾部可能比平均值更相关。
参考文献
-
S. Athey, G. Imbens, 异质因果效应的递归分割 (2016), 美国国家科学院院刊。
-
S. Wager, S. Athey, 使用随机森林估计和推断异质治疗效应 (2018), 美国统计协会期刊。
-
S. Athey, J. Tibshirani, S. Wager, 广义随机森林 (2019). 统计年鉴。
-
M. Oprescu, V. Syrgkanis, Z. Wu, 因果推断的正交随机森林 (2019). 第 36 届国际机器学习会议论文集。
相关文章
-
DAGs 和控制变量
-
匹配、加权还是回归?
-
理解元学习器
-
理解 AIPW,双重稳健估计量
-
理解因果树
代码
你可以在这里找到原始 Jupyter Notebook:
github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/causal_forests.ipynb
感谢阅读!
我非常感激! 🤗 如果你喜欢这篇文章并希望看到更多,请考虑 关注我。我每周发布一次关于因果推断和数据分析的内容。我尽量使我的帖子既简单又准确,始终提供代码、示例和模拟。
另外, 免责声明: 我写作是为了学习,因此错误是常有的事,尽管我尽力而为。如果你发现错误,请告诉我。我也欢迎对新话题的建议!
从集中式学习到联邦学习
关于在 CIFAR 基准数据集上进行联邦学习的数据集分布技术总结
·
关注 发表在 Towards Data Science ·9 min read·Mar 16, 2023
--
联邦学习 (FL) 是一种在分布式环境下训练机器学习(ML)模型的方法 [1]。其理念是客户端(例如医院)希望在不共享其私密和敏感数据的情况下合作。每个客户端在 FL 中保留其私有数据,并在这些数据上训练 ML 模型。然后,中央服务器收集并聚合模型参数,从而基于所有数据分布的信息构建一个全球模型。理想情况下,这作为设计上的隐私保护。
已经进行了大量研究以理解 FL 的效率、隐私和公平性。在这里,我们将重点关注用于评估水平 FL 方法的基准数据集,其中客户端共享相同的任务和数据类型,但他们拥有各自的数据样本。
如果你想了解更多关于联邦学习及我所做的工作,请访问我们的研究实验室网站!
文献中有三种类型的数据集:
-
真实的 FL 场景:FL 是一种需要的方法的应用场景。它具有自然分布和敏感数据。然而,考虑到 FL 的性质,如果你想将数据保存在本地,你不会将数据集在线发布以进行基准测试。因此,很难找到这种类型的数据集。OpenMined 背后的 PySyft 试图组织一个 FL 社区,由大学和研究实验室主办数据,以更现实的场景进行数据托管。此外,最近隐私意识有所提高的应用场景也存在。因此,可能会有公开的数据,而 FL 的需求依然存在。一个应用场景是智能电表[2]。
-
FL 基准数据集:这些数据集设计用于作为 FL 基准。分布是现实的,但数据的敏感性存在疑问,因为它们是从公开来源构建的。例如,从 Reddit 帖子中创建 FL 数据集,使用用户作为客户端,并将其分发给一个用户作为一个分区。LEAF 项目提出了更多类似的数据集[3]。
-
分发标准数据集:有一些众所周知的数据集,如 CIFAR 和 ImageNet,作为图像的例子,在许多机器学习工作中用作基准。在这里,FL 科学家根据他们的研究问题定义一个分布。如果主题在标准机器学习场景中研究得很透彻,并且希望将其 FL 算法与集中式 SOTA 进行比较,那么使用这种方法是有意义的。然而,这种人工分布并没有揭示分布偏斜的所有问题。例如,客户端收集的图像可能来自不同的相机或不同的光照条件。
由于最后一种类别不是设计上分布的,过去的研究将其分为几种方式。在接下来的内容中,我将总结在联邦场景中用于 CIFAR 数据集的分发技术。
CIFAR 数据集
CIFAR-10 和 CIFAR-100 数据集 包含 32x32 的彩色图像,并标记为互斥的类别 [4]。CIFAR-10 有 10 个类别,每个类别 6000 张图像,而 CIFAR-100 有 100 个类别,每个类别 600 张图像。它们在许多图像分类任务中使用,可以访问到基于这些数据集评估的数十个模型,甚至可以通过 PapersWithCode 的排行榜 浏览它们。
联邦学习中的数据划分
均匀分布
这被认为是 同分布且独立(IID)数据。数据点随机分配给客户端。
单类(n-)客户端
分配给特定客户端的数据点来自相同的类别或类别。这可以被认为是极端非独立同分布(non-IID)设置。这种分布的例子见 [1,5–8]。首先将这种设置命名为联邦学习 [1] 的工作使用了 200 个单类数据集,并给每个客户端两个数据集,使其成为 2 类客户端。[5–7] 使用 2 类客户端。
[9] 基于 CIFAR-100 中的层级类:客户端的数据点来自每个超级类中的一个子类。这样,在超级类的分类任务中,客户端拥有来自每个(超级)类的样本,但由于数据点来自不同的子类,因此模拟了分布偏斜。例如,一个客户端访问狮子的图像,而另一个客户端访问老虎的图像,超级类任务是将两者归类为大型食肉动物。
主导类别客户端
[5] 也使用了均匀和 2 类客户端的混合,这意味着一半的数据点来自 2 个主要类别,其余的数据点均匀地从其他类别中选择。[10] 使用 80%-20% 的划分,80% 来自单一主导类别,其余的从其他类别中均匀选择。
Dirichlet 分布
要了解 Dirichlet 分布,我参考了 这篇博客文章。假设有人想要制作一个骰子,θ=(1/6,1/6,1/6,1/6,1/6,1/6) 表示每个数字 1–6 的概率。然而,实际上没有什么是完美的,所以每个骰子会有些偏斜。例如,4 出现的可能性稍高,而 3 的可能性稍低。Dirichlet 分布用参数向量 α=(α₁,α₂,..,α₆) 描述这种多样性。较大的 αᵢ 增强了该数字的权重,而 αᵢ 值的整体总和较大则确保了更相似的抽样概率(骰子)。回到骰子例子,为了让骰子公平,每个 αᵢ 应该相等,且 α 值越大,骰子的制造越好。由于这是 beta 分布 的多变量推广,让我们展示一些 beta 分布的例子(Dirichlet 分布与两个骰子):
不同的 beta 分布(2 变量的 Dirichlet 分布) — 图由作者提供
我重现了 [11] 中的可视化,使用相同的 α 值为每个 αᵢ。这被称为 对称 Dirichlet 分布。我们可以看到,随着 α 值的减少,更有可能出现不平衡的骰子。下面的图显示了不同 α 值的 Dirichlet 分布。这里每一行代表一个类别,每一列代表一个客户端,圆圈的面积与概率成正比。
类别分布:使用不同的 Dirichlet 分布 α 值对 10 个类别的 20 个客户端进行采样 — 图由作者提供
类别分布:每个客户端的样本是独立抽取的,类别分布遵循 Dirichlet 方法。[11, 16] 使用这种 Dirichlet 分布版本。
类别分布:按类别(10)和客户端(20)归一化的样本总和 — 图由作者提供
每个客户端有一个预定数量的样本,但类别是随机选择的,因此最终的类别表示会不平衡。在客户端中,α→∞ 是先验(均匀)分布,而 α→0 意味着单类别客户端。
客户端分布:使用不同的 Dirichlet 分布 α 值对 10 个类别的 20 个客户端进行采样 — 图由作者提供
客户端分布:如果我们知道一个类别中的样本总数和客户端的数量,我们可以按类别将样本分配给客户端。这将导致客户端拥有不同数量的样本(在 FL 中非常典型),而全局类别分布则是平衡的。[12] 使用了这种 Dirichlet 分布的变体。
客户端分布:按类别(10)和客户端(20)归一化的样本总和 — 图由作者提供
尽管像 [11–16] 这样的工作相互引用使用 Dirichlet 分布,但它们使用了两种不同的方法。此外,不同的实验使用了不同的 α 值,这可能导致非常不同的性能。[11,12] 使用 α=0.1,[13-15] 使用 α=0.5,[16] 概述了不同的 α 值。这些设计选择丧失了使用相同基准数据集来评估算法的原始原则。
非对称 Dirichlet 分布:可以使用不同的 αᵢ 值来模拟更具资源的客户端。例如,下面的图是使用 1/i 为第 i 个客户端生成的。据我所知,这在文献中没有表示,而是使用 Zipf 分布 [17]。
非对称 Dirichlet 分布,αᵢ=1/i — 图由作者提供
Zipf 分布
[17] 使用了 Zipf 分布和 Dirichlet 分布的组合。它使用 Zipf 分布来确定每个客户端的样本数量,然后使用 Dirichlet 分布选择类别分布。
Zipf 分布中第k名的概率,其中是黎曼ζ函数
在 Zipf(zeta)分布中,某个项目的频率与其在频率表中的排名成反比。Zipf 定律可以在许多现实世界的数据集中观察到,例如语言语料库中的词频[18]。
使用 Zipf 分布抽样的项目 — 作者根据numpy Zipf 文档绘制的图
结论
基准测试联邦学习方法是一项具有挑战性的任务。理想情况下,人们会使用预定义的真实联邦数据集。然而,如果某种场景必须在没有良好的现有数据集来覆盖的情况下进行模拟,可以使用数据分布技术。为可重复性和设计选择的动机提供适当的文档是重要的。在这里,我总结了用于 FL 算法评估的最常见方法。访问这个 Colab 笔记本获取用于此故事的代码!
参考文献
[1] McMahan, B., Moore, E., Ramage, D., Hampson, S., & y Arcas, B. A. (2017 年 4 月). 从去中心化数据中高效学习深度网络. 载于人工智能与统计学(第 1273–1282 页)。PMLR。
[2] Savi, M., & Olivadese, F. (2021). 边缘端的短期能源消耗预测:一种联邦学习方法. IEEE Access, 9, 95949–95969。
[3] Caldas, S., Duddu, S. M. K., Wu, P., Li, T., Konečný, J., McMahan, H. B., … & Talwalkar, A. (2019). Leaf: 联邦设置的基准. 联邦学习数据隐私与保密研讨会
[4] Krizhevsky, A. (2009). 从微小图像中学习多层特征. 硕士论文, 特隆赫大学。
[5] Liu, W., Chen, L., Chen, Y., & Zhang, W. (2020). 通过动量梯度下降加速联邦学习. IEEE 并行与分布式系统汇刊, 31(8), 1754–1766。
[6] Zhang, L., Luo, Y., Bai, Y., Du, B., & Duan, L. Y. (2021). 通过统一特征学习和优化目标对齐进行非独立同分布数据的联邦学习. 载于IEEE/CVF 国际计算机视觉会议论文集(第 4420–4428 页)。
[7] Zhang, J., Guo, S., Ma, X., Wang, H., Xu, W., & Wu, F. (2021). 个性化联邦学习的参数化知识转移. 神经信息处理系统进展, 34, 10092–10104。
[8] Zhao, Y., Li, M., Lai, L., Suda, N., Civin, D., & Chandra, V. (2018). 带有非独立同分布数据的联邦学习. arXiv 预印本 arXiv:1806.00582。
[9] Li, D., & Wang, J. (2019)。Fedmd:通过模型蒸馏的异质联邦学习。arXiv 预印本 arXiv:1910.03581。
[10] Wang, H., Kaplan, Z., Niu, D., & Li, B. (2020 年 7 月)。使用强化学习优化非独立同分布数据上的联邦学习。载于IEEE INFOCOM 2020-IEEE 计算机通信会议(第 1698–1707 页)。IEEE。
[11] Lin, T., Kong, L., Stich, S. U., & Jaggi, M. (2020)。用于联邦学习中强健模型融合的集成蒸馏。神经信息处理系统进展,33,2351–2363。
[12] Luo, M., Chen, F., Hu, D., Zhang, Y., Liang, J., & Feng, J. (2021)。不惧异质性:用于非独立同分布数据的联邦学习分类器校准。神经信息处理系统进展,34,5972–5984。
[13] Yurochkin, M., Agarwal, M., Ghosh, S., Greenewald, K., Hoang, N., & Khazaeni, Y. (2019 年 5 月)。贝叶斯非参数化联邦学习的神经网络。载于国际机器学习会议(第 7252–7261 页)。PMLR。
[14] Wang, H., Yurochkin, M., Sun, Y., Papailiopoulos, D., & Khazaeni, Y. (2020) 使用匹配平均的联邦学习。载于国际学习表征会议。
[15] Li, Q., He, B., & Song, D. (2021)。模型对比的联邦学习。载于IEEE/CVF 计算机视觉与模式识别会议论文集(第 10713–10722 页)。
[16] Hsu, T. M. H., Qi, H., & Brown, M. (2019)。测量非相同数据分布对联邦视觉分类的影响。arXiv 预印本 arXiv:1909.06335。
[17] Wadu, M. M., Samarakoon, S., & Bennis, M. (2021)。在联邦学习中在频道不确定性下的联合客户端调度与资源分配。IEEE 通信交易,69(9),5962–5974。
[18] Fagan, Stephen; Gençay, Ramazan (2010),“文本计量经济学导论”,载于 Ullah, Aman; Giles, David E. A.(编),实证经济学与金融手册,CRC Press,第 133–153 页
从混乱到清晰:利用大型语言模型简化数据清洗
使用 OpenAI 的 GPT 模型清理调查回应。包含完整代码和 Github 链接。
·
关注 发表在 Towards Data Science ·17 min 阅读 ·2023 年 6 月 7 日
--
图片由 Dall-E 2 提供,生成并由作者修改。
在数字时代,准确可靠的数据对致力于提供个性化体验和做出明智决策的企业至关重要[1]。然而,数据的庞大数量和复杂性常常带来重大挑战,需要大量繁琐的手工工作。此时,改变游戏规则的大型语言模型(LLMs)技术登场。这些先进的 AI 工具凭借其自然语言处理能力和模式识别,有可能彻底改变数据清洗过程,使其更加可用。
在数据科学家的工具箱中,LLMs 就像是扳手和螺丝刀,重新塑造活动,利用其强大能力提高数据质量。隐喻中的锤子将揭示可操作的见解,并最终为更好的客户体验铺平道路。
话虽如此,让我们直接深入到今天将用作示例的用例中。
图片由 Scott Graham 提供,来源于 Unsplash
用例
在对学生进行调查时,最糟糕的事情就是把一个事实字段留作自由文本!你可以想象我们收到了哪些回应。
开玩笑的话题,我们的一位客户,Study Fetch,一个利用课程材料为学生创建个性化全能学习集的 AI 驱动平台,进行了一项针对大学生的调查。在收到超过 10K 的回应后,他们的 CEO 兼联合创始人 Esan Durrani 遇到了一点小麻烦。原来,调查中的“主要”字段是一个自由文本框,意味着受访者可以随意输入内容。作为数据科学家,我们知道如果你想进行统计分析,这不是最明智的做法。因此,调查的原始数据看起来就是这样的……
Anthropology
Chem E
Computer Science
Business and Law
Drama
cs
IMB
哦天哪!准备好拿起你的 Excel,开始一个排序冒险,这可能仅需一个小时,或者,谁知道,也许需要三小时。只有这样,这些数据异端才会被彻底清除。
然而,不用担心,因为我们有大型语言模型(LLM)的锤子。
正如一位智者曾说的,如果你只有一把锤子,那么一切看起来都像钉子。而且,数据清洗工作似乎就是最完美的钉子了。
我们可以简单地让我们友好的 LLM 将这些分类到已知的专业中。具体来说,OpenAI 的生成预训练变换器(GPT),一个支持流行的聊天机器人应用 ChatGPT 的 LLM,将适用于这种情况。GPT 模型使用超过 1750 亿个参数,并且已经在从 Common Crawl 提取的 26 亿个存储网页上进行了训练。此外,通过一种称为人类反馈的强化学习(RLHF)的技术,训练师可以引导和促使模型提供更准确和有用的响应。[2]
我认为对于我们的目的来说,1750 亿+ 参数应该足够了。只要我们能够提出正确的提示。
Kelly Sikkema 的照片,来源于 Unsplash
一切都在提示中
来自 AI 公司 Ryan 和 Esan,他们的专长是编写出色的提示,提出了我们提示的第一个版本。这个版本非常好,并且在使用语言推断[3]时效果很好,但还有两个方面可以改进:
-
这篇文章是为了一个记录而写的
-
它是作为‘完成’使用 Da Vinci 模型编写的(提到 IT 让我银行账户都吓了一跳)
这会花费我们太多,而且这显然是不行的。因此,Ryan 和我分别将提示重写为一个使用‘gpt-3.5-turbo’进行批量操作的聊天提示。OpenAI 的 提示最佳实践 和课程 ChatGPT Prompt Engineering for Developers 对我非常有帮助。我们经过了几次迭代的构思、实施、分析和改进,最终得到了一个好的工作版本。
不再废话,以下是修订版 2 后的提示:
1\. Arts and Humanities: Literature and arts.
2\. Social Sciences: psychology, sociology, and political science
3\. Business and Management: marketing, finance, economics, MBA
4\. Life Sciences: Biology, environmental,agriculture-related
5\. Engineering and Technology: Electrical, mechanical, etc.
6\. Medicine Health and Allied Sciences: Nursing, medical school
7\. All Other Programs: Everything else.
The above information pertains to categories of undergraduate programs.
Categorize the below data into one of the program categories above.
Desired Format: CSV of data row, program name
Data
###
Drama
Literature
Physics
Health sciences
Mechanical Engineering
###
对于这个提示,LLM 的响应是
Drama, Arts and Humanities: Literature and arts
IMB, All Other Programs: Everything else
Health sciences, Medicine Health and Allied Sciences: Nursing, medical school
MBA, Business and Management: marketing, finance, economics, MBA
Mechanical Engineering, Engineering and Technology: Electrical, mechanical, etc.
这将有效,虽然有点。 但我不太喜欢冗长的程序名称重复的冗余文本。对于 LLM,文本是标记,而标记是要花钱的。你看,我的编程技能是在互联网泡沫的熊熊烈火中锻造出来的。让我告诉你,我从不放过任何节省成本的机会。
所以,我在‘期望格式’部分稍微更改了提示。我要求模型仅输出调查响应的序号(例如上面的 1 代表戏剧)和程序的序号(例如 1 代表文学)。然后 Ryan 建议我请求 JSON 输出而不是 CSV,以便更容易解析。他还建议我添加一个‘示例输出’部分,这个建议非常好。
最终的提示如下(为了清晰简化):
1\. Arts and Humanities: Literature and arts.
2\. Social Sciences: psychology, sociology, and political science
3\. Business and Management: marketing, finance, economics, MBA
4\. Life Sciences: Biology, environmental,agriculture-related
5\. Engineering and Technology: Electrical, mechanical, etc.
6\. Medicine Health and Allied Sciences: Nursing, medical school
7\. All Other Programs: Everything else.
The above information pertains to categories of undergraduate programs.
Categorize the below data into one of the program categories above.
Desired Format: JSON format of an array with response number, program number
Example Example output: [[1,7],[2,1],[3,5],[4,8],[5,7]]
Data
###
1\. Drama
2\. IMB
3\. Health sciences
4\. MBA
5\. Mechanical Engineering
###
我们使用的完整提示可以在 GitHub 链接这里查看。
模型的输出:
[
[1, 1]
[2, 3]
[3, 6]
[4, 3]
[5, 5]
]
正如之前讨论的那样,模型的输出是调查响应的序数与我们定义的类别之间的映射。以第一行举例:1,1。那意味着 1 是响应号,1 是对应的映射程序号。调查响应 1 是“戏剧”,映射程序 1 是“艺术与人文学科”。这似乎是对的!戏剧在其正确的#1 位置,所有的目光都集中在它上面。
尽管输出乍看起来像是嵌入(用于聚类和降维)的结果,但它们只是带有序数位置的相同映射信息。除了在令牌使用上提供一些成本优势外,这些数字也更容易解析。
我们现在可以将文件中的原始调查响应翻译为有意义的专业,进行汇总,并获得有价值的可操作见解。
但等等,我不打算坐在电脑前,将每一块调查响应输入到浏览器中并计算映射。除了令人厌烦外,错误率也完全无法接受。
我们需要一些老式的自动化。进入 API…
API 的救援
如你所知,应用程序编程接口(API)允许我们的程序高效地与第三方服务互动。尽管许多人在使用 ChatGPT 时取得了令人印象深刻的成就,但语言模型的真正潜力在于利用 API 将自然语言能力无缝集成到应用程序中,使其对用户不可察觉。这就像你用来阅读这篇文章的手机或电脑中所蕴含的不可思议的科技。
如果你还没有,可以在这里申请 API 访问,openai.com/blog/openai-api
[4]。一旦注册并获得 API 密钥,可以在这里找到规格说明。包含代码示例的一些非常有用的示例可以在这里找到。playground是一个很好的功能,可以在将其投入使用前测试各种设置。
我们将使用 REST 的聊天完成 API。调用的示例有效负载如下:
{
“model”: “gpt-3.5-turbo”,
“temperature”: 0,
"n": 1,
“messages”: [
{“role”: “user”, “content”: “Hello, I'm a nail.”}
]
}
让我们快速查看一下参数及其效果
模型
目前唯一对公众开放的聊天完成模型是 gpt-3.5-turbo。Esan 有访问 GPT 4 模型的权限,这让我非常嫉妒。虽然 gpt-4 更加准确,并且幻觉更少[2],但它的费用大约是 gpt-3.5-turbo 的 20 倍。对于我们的需求来说,Turbo 先生已经相当合适了,谢谢。
温度
在提示旁边,温度是我们可以传递给模型的最重要设置之一。根据 API 文档,它可以设置为 0 到 2 之间的值。它有着显著的影响[6],因为它控制输出的随机性,类似于你在开始写作前体内的咖啡因含量。关于每个应用程序可使用的值的指南在这里给出[7]。
对于我们的使用案例,我们仅仅希望没有变化。我们希望引擎每次都给出完全一样的映射。因此,我们使用了 0 的值。
n
要生成多少个聊天完成选项?如果我们是在进行创意写作,并且希望选择的选项超过 1 个,我们可以使用 2 甚至 3 个。对于我们的情况,n=1(默认)效果很好。
消息
角色可以是系统、用户或助手。系统角色提供指令并设定上下文。用户角色代表最终用户的提示。助手角色基于对话历史进行回应。这些角色有助于构建对话,并实现用户与 AI 助手之间的有效互动。
模型最大令牌
这不一定是我们在请求中传递的参数,不过另一个名为 max_tokens 的参数限制了聊天响应的总长度。
首先,令牌可以被认为是一个单词的一部分。一个令牌大约是 4 个英文字符。例如,引用“预见未来的最佳方式是创造未来”,这个引言被归因于亚伯拉罕·林肯等人,包含了 11 个令牌。
图片来源于 Open AI Tokenizer。由作者生成。
如果你认为一个令牌就是一个单词,这里有另外一个 64 个令牌的例子,来说明这并不是那么简单。
图片来源于 Open AI Tokenizer。由作者生成。
准备好接受震撼的揭示:你在消息中包含的每一个表情符号都增加了最多 6 的费用。没错,你喜爱的笑脸和眨眼符号都是狡猾的小令牌窃贼!😉💸
模型的最大令牌窗口是一个技术限制。你的提示(包括你放入的任何额外数据)和答案必须都在模型最大限制内,详细信息请查看这里。在聊天完成的情况下,内容、角色和所有之前的消息都消耗令牌。如果你从输入或输出(助手消息)中删除一条消息,模型将失去对其的所有记忆[8]。就像多莉在寻找奇科时一样,没有法比奥,没有宾戈,没有哈波,没有艾尔莫?… 尼莫!
对于 gpt-3.5-turbo,模型的最大限制是 4096 个 token,或者大约 16K 字符。对于我们的用例,提示大约是 2000 个字符,每个调查回复大约是 20 个字符(平均),映射回复是 7 个字符。因此,如果我们在每个提示中放入 N 个调查回复,最大字符数将是:
2000 + 20N + 7N 应小于 16,000。
解算后,我们得到一个小于 518 或大约 500 的 N 值。从技术上讲,我们可以在每个请求中放入 500 个调查回复,并处理数据 20 次。相反,我们选择每个请求中放入 50 个调查回复,处理 200 次,因为如果我们在单个请求中放入超过 50 个调查回复,偶尔会收到异常回复。有时,服务会发脾气!我们不确定这是系统性乖戾的慢性病,还是刚好碰上了运气不好的一面。
那么,我们如何使用这个 API 呢?让我们进入重点,代码部分。
代码的方式
Node.js 是一个 JavaScript 运行环境[9]。我们将编写一个 Node.js/JavaScript 程序,该程序将执行在这个流程图中描述的操作:
程序的流程图。图片由作者提供。
我的 JavaScript 技能不是特别出色。我可以写更好的 Java、PHP、Julia、Go、C#,甚至 Python。但 Esan 坚持使用 Node,所以就用 JavaScript 吧。
所有代码、提示和示例输入可以在这个GitHub 链接中找到。不过,让我们来看看最重要的部分:
首先,让我们看看如何使用“csv-parser”Node 库读取 CSV 文件。
fs.createReadStream(fileName)
.pipe(csv())
.on('data', (data) => rows.push(data))
.on('end', async () => {
// Reading is done. Call classifier here
. . .
}
接下来,我们调用分类器生成映射。
for(index = 0; index < totalDataPoints; ++index) {
dataText += (index+1).toString() + ". "
+ uniqueData[index] + "\n";
requestCount++;
if (requestCount>batchSize||index==totalDataPoints-1){
dataText += "###\n";
// We have loaded batchSize(50) response.
// Now construct the prompt
...
}
}
然后,提示由类别、主要提示文本和 CSV 中的数据构成。我们将提示通过他们的 OpenAI Node 库发送到服务。
let prompt = categoriesText + mainPrompt + dataText;
let payload = {
model: "gpt-3.5-turbo",
temperature: 0,
messages: [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user","content": prompt }
]
};
try {
const response = await openai.createChatCompletion(payload);
let mapping = JSON.parse(response.data.choices[0].message.content);
// Here save the mappings
...
} catch (error) {
console.error('API Error:',error);
}
最后,当所有迭代完成后,我们可以将 srcCol 文本(调查回复)翻译为 targetCol(标准化的程序名称),并写出 CSV 文件。
for (let row of rows) {
srcVal = row[srcCol].trim();
if (!row[targetCol] && dataMapping[srcVal])
row[targetCol] = dataMapping[srcVal];
}
stringify(rows, {
header: true
}, function (err, output) {
fs.writeFile(__dirname+'/'+destFileName, output,
function(err, result) {
if(err) console.log('error', err);
});
});
那段 JavaScript 代码没有我想象中的那么复杂,2 到 3 小时内完成了。我想,直到开始做之前总是看起来很令人生畏。
现在,代码已经准备好,是时候进行最终执行了……
执行
现在,我们需要一个地方来运行代码。在争论是否应该获得一个云实例来运行负载后,我做了一些快速计算,发现我可以在不到一小时内在我的笔记本电脑上运行它。这也不算太糟糕。
我们开始了测试轮次,并注意到服务有 1/10 的几率会返回提供给它的数据,而不是映射。因此,我们只会收到调查回应的列表。由于没有找到映射,这些 CSV 文件中的回应会被映射为空字符串。
与其在代码中检测并重试,我决定重新运行脚本,但只处理目标列为空的记录。
脚本将从所有行中目标列为空开始,并填写标准化的程序名称。由于响应中的错误,一些行的目标列没有被映射并保持为空。当脚本第二次运行时,它只会为第一次运行中未处理的回应构造提示。我们重跑了几次程序,最终将所有内容映射完成。
多次运行大约花了 30 分钟,并且不需要太多监督。这里是模型的一些更有趣的映射的选择:
输入和程序名称之间的示例映射。图片由作者提供。
大多数看起来对。组织行为学是社会科学还是商业学科呢?我想两者都可以。
每个约 50 条记录的请求总共使用了大约 800 个 tokens。整个过程的费用是 40 美分。我们可能花了 10 美分进行测试、重跑等。所以,总费用约 50 美分,大约 2 ½小时的编码/测试时间和½小时的运行时间,我们完成了工作。
总费用: 约 1 美元以内
总时间: 大约 3 小时
也许使用 Excel、排序、正则表达式以及拖拽复制的手动转换,我们可以在同样的时间内完成并节省一点费用。但是,这要有趣得多,我们学到了东西,我们有了一个可重复的脚本/过程,并且还写了一篇文章。而且,我觉得StudyFetch可以承担这 50 美分。
这是我们高效且经济地实现的一个好用例,但 LLMs 还能用于什么其他用途呢?
照片由Marcel Strauß提供,来自Unsplash
寻找更多的钉子
为你的应用程序添加语言能力可能会有比我上面所展示的更多的用途。这里是一些仅与我们查看的审核数据相关的更多用例:
数据解析和标准化:LLMs 可以通过识别和提取来自非结构化或半结构化数据源的相关信息,来帮助解析和标准化数据,例如我们刚刚查看的数据。
数据去重:LLMs 可以通过比较各种数据点来帮助识别重复记录。例如,我们可以比较评论数据中的姓名、专业和大学,并标记潜在的重复项。
数据总结:LLMs 可以总结不同的记录,以了解响应情况。例如,对于“你在学习过程中面临的最大挑战是什么?”这个问题,大型语言模型可以总结来自相同专业和大学的多个回答,以查看是否存在任何模式。然后我们可以将所有总结放入一个请求中,获取总体列表。但我怀疑对每个客户细分市场的总结会更有用。
情感分析:LLMs 可以分析评论以确定情感并提取有价值的见解。对于“你会为帮助你学习的服务付费吗?”这个问题,LLMs 可以将情感分类为 0(非常负面)到 5(非常积极)。我们可以利用这一点按细分市场分析学生对付费服务的兴趣。
尽管学生评价是一个较小的缩影,但这个技术在更广泛的领域也有许多用途。在我工作的AAXIS公司,我们实施了企业对企业和企业对消费者的数字商务解决方案。这包括将大量数据从现有的旧系统迁移到具有不同数据结构的新系统。我们使用各种数据工具来分析源数据,以确保一致性。本文中概述的技术可能会对这个目的有很大帮助。
其他一些数字商务用例包括检查产品目录中的错误、编写产品文案、扫描评论回复和产品评论总结等等。比起当询问他们的专业时的本科生创意,编程要简单得多。
尽管大型语言模型(LLMs)在数据清洗中可能是强大的工具,但仍需注意,它们应该与其他技术和人工监督结合使用。数据清洗过程通常需要领域专业知识、上下文理解和手动审查,以做出明智的决策并保持数据完整性。LLMs 也不是推理引擎[10]。它们是下一个词预测器。它们往往非常自信且令人信服地提供错误信息(幻觉)[2][11]。幸运的是,在我们的测试过程中,我们没有遇到任何幻觉,因为我们的用例主要涉及分类。
如果谨慎使用并了解潜在陷阱,LLMs 可以成为你工具箱中的一个很好的工具。
图片由 Paul Szewczyk 提供,来自 Unsplash
最终的关键
在这篇文章中,我们首先探讨了数据清理的具体用例:将调查回应标准化为一组特定值。这将使我们能够对回应进行分组,并获得有价值的见解。我们使用了大型语言模型(LLM),Open AI 的 GPT 3.5 Turbo,来帮助分类这些回应。我们回顾了使用的提示、如何利用提示进行 API 调用以及所需的代码来自动化所有操作。最后,我们将所有内容整合在一起,总共花费的 OpenAI 工具成本不到一美元。
我们是否拥有一个典型的大型语言模型工具,并找到完美的解决方案?也许。但更可能的是,我们拥有了一把瑞士军刀,并用它来剥皮和吃鱼。虽然不是特别专业,但仍然非常合适。至于 Esan,真的很喜欢寿司。
你的用例是什么?我们很想听听你的意见!
同谋者
本文的主要工作由我 Esan Durrani 和 Ryan Trattner 执行,我们是 StudyFetch 的联合创始人,该平台利用课程材料为学生创建个性化的全能学习集。
我还要感谢 Prashant Mishra、Rajeev Hans、Israel Moura 和 Andy Wagner,我在 AAXIS Digital 的同事们,感谢他们对本文的审阅和建议。
我还要感谢我三十年的朋友 基兰·邦达拉帕提,TRM Labs 的工程副总裁,感谢他在生成 AI 领域的初步指导和对本文的审阅。
另外,感谢我的编辑 梅根·波尔斯特拉,一如既往地使文章看起来和感觉专业。
参考文献
1. 泰穆·赖塔卢奥托,《数字时代个性化营销的重要性》,MaketTailor Blog,2023 年 5 月,www.markettailor.io/blog/importance-of-personalized-marketing-in-digital-age
2. 安库尔·A·帕特尔、布赖恩特·林顿 和 迪娜·索斯塔雷克,《GPT-4、GPT-3 和 GPT-3.5 Turbo:OpenAI 大型语言模型综述》,2023 年 4 月,Ankur’s Newsletter,www.ankursnewsletter.com/p/gpt-4-gpt-3-and-gpt-35-turbo-a-review
3. 亚历山德拉·门德斯,《终极 ChatGPT 提示工程指南:面向普通用户和开发者》,2023 年 6 月,Imaginary Cloud Blog,www.imaginarycloud.com/blog/chatgpt-prompt-engineering/
4. 塞巴斯蒂安,《如何在 Node.js 中使用 OpenAI 的 ChatGPT API》,2023 年 3 月,Medium — 智能编程方式,medium.com/codingthesmartway-com-blog/how-to-use-openais-chatgpt-api-in-node-js-3f01c1f8d473
5. 特里斯坦·沃尔夫,《利用 OpenAI API Playground 解放你的 ChatGPT 提示》,2023 年 2 月,Medium — 明日故事,medium.com/tales-of-tomorrow/liberate-your-prompts-from-chatgpt-restrictions-with-the-openai-api-playground-a0ac92644c6f
6. AlgoWriting,《简单指南:设置 GPT-3 温度》,2020 年 11 月,Medium,algowriting.medium.com/gpt-3-temperature-setting-101-41200ff0d0be
7. 凯恩·胡珀,《用 Ruby 掌握 GPT-3 温度参数》,2023 年 1 月,Plain English,plainenglish.io/blog/mastering-the-gpt-3-temperature-parameter-with-ruby
8. OpenAI 作者,《GPT 指南 — 管理令牌》,2023 年,OpenAI 文档,platform.openai.com/docs/guides/gpt/managing-tokens
9. 普里耶什·帕特尔,《Node.js 究竟是什么?》,2018 年 4 月,Medium — 自由代码营,medium.com/free-code-camp/what-exactly-is-node-js-ae36e97449f5
10. 本·迪克森,《大型语言模型存在推理问题》,2022 年 6 月,Tech Talks Blog,bdtechtalks.com/2022/06/27/large-language-models-logical-reasoning/
11. 弗兰克·诺伊根鲍尔,《理解 LLM 幻觉》,2023 年 5 月,Towards Data Science,towardsdatascience.com/llm-hallucinations-ec831dcd7786
数据科学中的 Docker
原文:
towardsdatascience.com/from-chaos-to-consistency-docker-for-data-scientists-240372adff18
数据科学家用 Docker 的介绍和应用
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 24 日
--
图片由 Ian Taylor 提供,来源于 Unsplash
背景
但在我的机器上运行正常?
这是技术社区中的经典梗,尤其对于那些想要发布他们惊人的机器学习模型的数据科学家来说,结果却发现生产机器上有不同的操作系统。远非理想。
然而…
多亏了这些被称为容器的奇妙事物和控制它们的工具,如Docker,这才有了解决方案。
在这篇文章中,我们将深入探讨容器是什么,以及如何使用 Docker 构建和运行它们。容器和 Docker 的使用已成为行业标准和数据产品的常见实践。作为数据科学家,学习这些工具是你武器库中宝贵的工具。
什么是 Docker?
Docker 是一种服务,可以在容器中构建、运行和执行代码及应用程序。
现在你可能在想,什么是容器?
表面上看,容器非常类似于虚拟机 (VM)。它是一个小型隔离环境,一切都自‘包含’,可以在任何机器上运行。容器和虚拟机的主要卖点是它们的便携性,允许你的应用程序或模型在任何本地服务器、个人计算机或如AWS这样的云平台上无缝运行。
容器和虚拟机之间的主要区别在于它们如何使用主机计算机的资源。容器要轻得多,因为它们不会主动分割主机计算机的硬件资源。我这里不深入探讨所有技术细节,但如果你想了解更多,我在这里链接了一篇很棒的文章解释它们的区别。
Docker 简单来说是我们用来轻松创建、管理和运行这些容器的工具。这是容器变得非常流行的主要原因之一,因为它使开发者能够轻松部署可以在任何地方运行的应用程序和模型。
作者提供的图示。
Docker 技术特性
我们运行容器所需的三个主要元素是:
-
Dockerfile: 一个包含如何构建 Docker 镜像的指令的文本文件。
-
Docker 镜像: 一个创建 Docker 容器的蓝图或模板。
-
Docker 容器: 一个提供应用程序或机器学习模型运行所需的一切的隔离环境。包括依赖项和操作系统版本等。
作者提供的图示。
还有一些其他关键点需要注意:
-
Docker 守护进程: 一个后台进程(守护进程),处理对 Docker 的传入请求。
-
Docker 客户端: 一个允许用户通过其守护进程与 Docker 进行交互的 shell 接口。
-
DockerHub: 类似于 GitHub,开发者可以在这里分享他们的 Docker 镜像。
安装 Docker
Hombrew
首先你应该安装的是 Homebrew (link here)。它被称为“MacOS 的缺失包管理器”,对任何在 Mac 上编码的人都非常有用。
要安装 Homebrew,只需运行他们网站上给出的命令:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
通过运行brew help
来验证 Homebrew 是否已安装。
Docker
现在 Homebrew 已安装,你可以通过运行brew install docker
来安装 Docker。通过运行which docker
来验证 Docker 是否已安装,输出结果不应出现任何错误,应该如下所示:
/opt/homebrew/bin/docker
Colima
最后一步是安装 Colima。 只需运行install colima
,并通过which colima
验证它是否已安装。再次,输出结果应如下所示:
/opt/homebrew/bin/colima
现在你可能会想,Colima 到底是什么?
Colima 是一个软件包,能够在 MacOS 上启用容器运行时。简单来说,Colima 为容器在我们的系统上工作创建了环境。为此,它运行一个带有守护进程的 Linux 虚拟机,Docker 可以通过客户端-服务器模型与其通信。
或者,你也可以安装Docker 桌面版来替代 Colima。然而,我更喜欢 Colima,原因有几个:它免费、更轻量,而且我喜欢在终端中工作!
有关 Colima 的更多论点,请查看这个博客文章。
使用 Docker 的示例
工作流
以下是数据科学家和机器学习工程师如何使用 Docker 部署其模型的示例:
作者的图示。
第一步显然是构建他们的惊人模型。然后,你需要将运行模型所需的所有内容打包起来,比如 Python 版本和包依赖。最后一步是使用 Dockerfile 中的 requirements 文件。
如果你现在觉得这完全随意,不用担心,我们会一步一步讲解这个过程!
基本模型
我们先从构建一个基本模型开始。提供的代码片段展示了在著名的Iris 数据集上实现随机森林分类模型的简单实现。
作者的 GitHub Gist。
这个文件叫做basic_rf_model.py
,供参考。
创建需求文件
现在我们有了模型,我们需要创建一个requirement.txt
文件,来容纳支撑我们模型运行的所有依赖。在这个简单的例子中,我们幸运地只依赖于scikit-learn
包。因此,我们的requirement.txt
文件看起来会是这样:
scikit-learn==1.2.2
你可以通过scikit-learn --version
命令检查你计算机上运行的版本。
创建 Dockerfile
现在我们终于可以创建我们的 Dockerfile 了!
因此,在与requirement.txt
和basic_rf_model.py
相同的目录下,创建一个名为Dockerfile
的文件。在Dockerfile
中,我们将包含以下内容:
作者的 GitHub Gist。
让我们逐行分析,看看这是什么意思:
-
FROM python:3.9
:这是我们镜像的基础镜像 -
MAINTAINER egor@some.email.com
:这表示谁维护这个镜像 -
WORKDIR /src
:将镜像的工作目录设置为 src -
COPY . .
:将当前目录的文件复制到 Docker 目录 -
RUN pip install -r requirements.txt
:将requirement.txt
文件中的要求安装到 Docker 环境中 -
CMD ["python", "basic_rf_model.py"]
:告诉容器执行命令python basic_rf_model.py
并运行模型
启动 Colima 和 Docker
下一步是设置 Docker 环境:首先我们需要启动 Colima:
colima start
Colima 启动后,通过运行以下命令检查 Docker 命令是否有效:
docker ps
它应该返回类似这样的内容:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
这很好,意味着 Colima 和 Docker 按预期工作!
注意:
docker ps
命令列出所有当前运行的容器。
构建镜像
现在是时候从我们上面创建的 Dockerfile
构建我们的第一个 Docker 镜像了:
docker build . -t docker_medium_example
-t
标志表示镜像的名称,*.*
告诉我们从当前目录构建。
如果我们现在运行docker images
,我们应该会看到类似这样的内容:
作者提供的图片。
恭喜,镜像已构建完成!
运行容器
在镜像创建完成后,我们可以使用上面列出的 IMAGE ID
作为容器运行它:
docker run bb59f770eb07
输出:
Accuracy: 0.9736842105263158
因为它所做的只是运行了basic_rf_model.py
脚本!
额外信息
本教程只是触及了 Docker 的基本功能和用途。要真正理解 Docker,还有许多功能和命令需要学习。Docker 网站上提供了一个详细的教程,你可以在这里找到。
一个很酷的功能是你可以以交互模式运行容器并进入其 shell。例如,如果我们运行:
docker run -it bb59f770eb07 /bin/bash
你将进入 Docker 容器,应该会看到类似这样的内容:
作者提供的图片。
我们还使用了ls
命令来显示 Docker 工作目录中的所有文件。
总结与进一步思考
Docker 和容器是确保数据科学家的模型可以随时随地运行的绝佳工具。它们通过创建包含模型有效运行所需的所有内容的小型隔离计算环境来实现这一点。这被称为容器。它易于使用且轻量化,使其成为如今常见的工业实践。本文介绍了如何使用 Docker 将模型打包到容器中的基本示例。这个过程简单而无缝,数据科学家可以很快学习和掌握。
本文使用的完整代码可以在我的 GitHub 上找到:
[## Medium-Articles/Software Engineering /docker-example at main · egorhowell/Medium-Articles
你现在无法执行此操作。你在另一个标签页或窗口中登录。你在另一个标签页或窗口中退出了……
另一个事项!
我有一个免费的通讯,Dishing the Data,在其中我每周分享成为更优秀数据科学家的技巧。没有“浮夸”或“点击诱饵”,只有来自实践中的数据科学家的纯粹可操作见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的 Dishing The Data,一份 Substack 出版物。
与我联系!
参考资料与进一步阅读
-
Docker 网站:
www.docker.com/
-
Docker 教程:
docker-curriculum.com/
-
Docker 常见技巧:
github.com/veggiemonk/awesome-docker
从簇到洞察;下一步
原文:
towardsdatascience.com/from-clusters-to-insights-the-next-step-1c166814e0c6
了解如何定量检测哪些特征驱动了簇的形成
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 10 日
--
图片由作者提供。
聚类分析是一种识别具有相似模式的组的优秀技术。然而,一旦簇形成后,确定簇背后的驱动特征可能依然具有挑战性。但这一步骤对于揭示之前可能遗漏的宝贵见解至关重要,这些见解可以用于决策制定和更深入地理解数据集。确定驱动特征的一种方法是通过特征值对样本进行着色。尽管这种方法很有洞察力,但当特征数量达到数百时,这种方法就显得劳动密集。此外,随着簇的大小和密度的不同,判断特定特征集的确切贡献可能会很困难。我将演示如何定量检测簇背后的驱动特征。 在这篇博客中, clusteval 库被用来进行聚类评估,并确定驱动簇形成的特征。
背景
无监督聚类是一种在数据中识别自然或数据驱动的组的技术,而无需使用预定义的标签或类别。聚类方法的挑战在于,不同的方法可能会由于对数据施加的隐含结构而导致不同的分组。要确定什么构成了“好”的聚类,我们可以使用定量指标。更多详细信息可以在博客“从数据到聚类;你的聚类什么时候足够好?”中阅读 [*1]*。
使用聚类方法可以找到合理的簇和隐藏的宝石,但你需要正确的聚类评估方法!
[towardsdatascience.com
clusteval
库测试特征是否与簇标签显著关联。
Clusteval是一个 Python 包,用于评估聚类的倾向、质量、簇的数量,并确定簇与特征之间的统计关联。Clusteval返回最佳簇标签的簇标签,以产生最佳的样本划分。实现了以下评估策略:轮廓系数、Davies-Bouldin 指数和导数(或肘部)方法,这些可以与K 均值、凝聚聚类、DBSCAN 和 HDBSCAN [1**]*结合使用。
pip install clusteval
为了检测簇标签背后的驱动特征,HNET库 [2] 在clusteval中被利用,执行对分类特征的超几何检验和对连续值的曼-惠特尼 U 检验,以评估特征是否与簇标签显著关联。更多详细信息可以在这里阅读:
探索以了解你的数据可以决定一个项目是否成功!
[towardsdatascience.com
确保聚类结果是可靠的。
在我们能够检测簇背后的驱动特征之前,我们首先需要对数据进行聚类,并确信我们的聚类是有效的。与监督方法相对,聚类算法处理的是同质数据,其中所有变量具有相似的类型或测量单位。这非常重要,因为聚类算法基于数据点的相似性对其进行分组,因此在混合数据类型或使用非同质数据时会产生不可靠的结果。确保以下几点:
-
数据根据研究目标和数据的统计特性进行标准化。
-
使用适当的距离度量。
-
评估簇的倾向和质量。
有了簇标签,我们可以调查特征的贡献。让我们在下一节中做一个小的用例。
玩具示例揭示簇标签背后的驱动特征。
对于这个用例,我们将加载在线购物者意图数据集,并进行预处理、聚类、评估,然后确定与簇标签显著相关的特征。此数据集包含 12330 个样本和 18 个特征。此混合数据集需要更多的预处理步骤,以确保所有变量具有相似的类型或测量单位。因此,第一步是创建具有可比单位的同质数据集。常见的方法是离散化并创建一个一次性矩阵。我将使用df2onehot
库,按照以下预处理步骤进行离散化:
-
分类值
0
、None
、?
和False
被移除。 -
少于 50 个正值的一次性特征被移除。
-
对于具有 2 个类别的特征,仅保留一个。
-
特征值独特性达到 80%或以上的被认为是数值型的。
预处理步骤将数据集转换为一个包含相同 12330 个样本但现在具有 121 个一次性特征的矩阵。值得注意的是,上述标准并不是绝对标准,而是应根据每个用例进行探索。对于聚类,我们将使用agglomerative
聚类方法,并以hamming
距离和complete
连通性进行聚类。请参见下面的代码部分。
# Intall libraries
pip install df2onehot
# Import libraries
from clusteval import clusteval
from df2onehot import df2onehot
# Load data from UCI
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00468/online_shoppers_intention.csv'
# Initialize clusteval
ce = clusteval()
# Import data from url
df = ce.import_example(url=url)
# Preprocessing
cols_as_float = ['ProductRelated', 'Administrative']
df[cols_as_float]=df[cols_as_float].astype(float)
dfhot = df2onehot(df, excl_background=['0.0', 'None', '?', 'False'], y_min=50, perc_min_num=0.8, remove_mutual_exclusive=True, verbose=4)['onehot']
# Initialize using the specific parameters
ce = clusteval(evaluate='silhouette',
cluster='agglomerative',
metric='hamming',
linkage='complete',
min_clust=2,
verbose='info')
# Clustering and evaluation
results = ce.fit(dfhot)
# [clusteval] >INFO> Saving data in memory.
# [clusteval] >INFO> Fit with method=[agglomerative], metric=[hamming], linkage=[complete]
# [clusteval] >INFO> Evaluate using silhouette.
# [clusteval] >INFO: 100%|██████████| 23/23 [00:28<00:00, 1.23s/it]
# [clusteval] >INFO> Compute dendrogram threshold.
# [clusteval] >INFO> Optimal number clusters detected: [9].
# [clusteval] >INFO> Fin.
在数据集上运行clusteval后,返回了 9 个簇。 由于数据包含 121 个维度(特征),我们无法直接在散点图中可视化检查簇。然而,我们可以进行嵌入,然后使用散点图进行视觉检查,如下面代码部分所示。指定embedding='tsne'
时,嵌入会自动执行。
# Plot the Silhouette and show the scatterplot using tSNE
ce.plot_silhouette(embedding='tsne')
图 1. 左面板:显示了带有检测到的簇和标签的轮廓评分图。右面板:散点图中样本按簇标签上色。两面板中的颜色和簇标签是匹配的。图片由作者提供。
图 1(右面板)的结果展示了 t-SNE 嵌入后的散点图,样本按簇标签上色。左面板展示了轮廓图,我们可以在其中视觉评估聚类结果的质量,如聚类的同质性、簇的分离情况以及通过聚类算法检测到的最佳簇数量。
此外,轮廓评分范围为-1 到 1(x 轴),其中接近 1 的评分表示簇内的数据点彼此非常相似,与其他簇的数据点不相似。簇 0、2、3 和 5 表示为良好分离的簇。接近 0 的轮廓评分表示簇之间的重叠或数据点与自身簇和邻近簇同样相似。接近-1 的评分则表明数据点与邻近簇的数据点更相似,而不是与自身簇的数据点。
条形的宽度代表了每个簇的密度或大小。较宽的条形表示簇较大且数据点更多,而较窄的条形表示簇较小且数据点较少。虚线红线(在我们的情况下接近 0)表示所有数据点的平均轮廓得分。它作为评估整体聚类质量的参考。平均轮廓得分高于虚线的簇被认为是分离良好的,而得分低于虚线的簇可能表示聚类效果较差。一般来说,好的聚类应该具有接近 1 的轮廓得分,表示簇的分离度良好。然而,请注意,我们现在已经在高维空间中对数据进行了聚类,并在低维 2D 空间中的 t-SNE 嵌入后评估了聚类结果。投影可能会给现实带来不同的视角。
另外,我们也可以先进行嵌入,然后在低维空间中对数据进行聚类(见下方代码部分)。现在我们将使用Euclidean
距离度量,因为我们的输入数据不再是独热编码,而是来自 t-SNE 映射的坐标。经过拟合后,我们检测到 27 个簇的最佳数量,这比我们之前的结果多得多。我们可以看到簇评估得分(图 2)似乎有些不稳定。这与数据的结构以及是否能够形成最佳聚类有关。
# Initialize library
from sklearn.manifold import TSNE
xycoord = TSNE(n_components=2, init='random', perplexity=30).fit_transform(dfhot.values)
# Initialize clusteval
ce = clusteval(cluster='agglomerative', metric='euclidean', linkage='complete', min_clust=5, max_clust=30)
# Clustering and evaluation
results = ce.fit(xycoord)
# Make plots
ce.plot()
ce.plot_silhouette()
图 2. 簇评估得分(得分越高越好)。
图 3. 左侧面板:显示检测到的簇及标签的轮廓得分图。右侧面板:样本在簇标签上的散点图。两面板之间的颜色和簇标签是匹配的。图像由作者提供。
轮廓图现在显示出比以前更好的结果,表明簇的分离度更好。在下一部分,我们将检测哪些特征与簇标签显著相关。
确定最佳簇数后,接下来是具有挑战性的一步;理解哪些特征驱动了簇的形成。
识别簇标签背后的驱动特征。
此时,我们检测到了每个样本被分配的最佳聚类数。为了检测聚类标签背后的驱动特征,我们可以计算特征与检测到的聚类标签之间的统计关联。这将确定某些变量的特定值是否倾向于与一个或多个聚类标签同时出现。各种统计关联测量,如卡方检验、费舍尔精确检验和超几何检验,通常用于处理有序或名义变量。我将使用超几何检验来测试类别变量与聚类标签之间的关联,使用Mann-Whitney U 检验来测试连续变量与聚类标签之间的关联。这些检验在HNET中可以方便地实现,而该工具也被用于clusteval库中。通过enrichment
功能,我们现在可以测试统计显著的关联。在这一步之后,我们可以使用散点图功能将丰富的特征绘制到聚类图上。
# Enrichment between the detected cluster labels and the input dataframe
enrichment_results = ce.enrichment(df)
# [df2onehot] >Auto detecting dtypes.
# 100%|██████████| 18/18 [00:00<00:00, 53.55it/s]
# [df2onehot] >Set dtypes in dataframe..
# [hnet] >Analyzing [cat] Administrative...........................
# [hnet] >Analyzing [num] Administrative_Duration...........................
# [hnet] >Analyzing [cat] Informational...........................
# [hnet] >Analyzing [num] Informational_Duration...........................
# [hnet] >Analyzing [cat] ProductRelated...........................
# [hnet] >Analyzing [num] ProductRelated_Duration...........................
# [hnet] >Analyzing [num] BounceRates...........................
# [hnet] >Analyzing [num] ExitRates...........................
# [hnet] >Analyzing [num] PageValues...........................
# [hnet] >Analyzing [num] SpecialDay...........................
# [hnet] >Analyzing [cat] Month...........................
# [hnet] >Analyzing [cat] OperatingSystems...........................
# [hnet] >Analyzing [cat] Browser...........................
# [hnet] >Analyzing [cat] Region...........................
# [hnet] >Analyzing [cat] TrafficType...........................
# [hnet] >Analyzing [cat] VisitorType...........................
# [hnet] >Analyzing [cat] Weekend...........................
# [hnet] >Analyzing [cat] Revenue...........................
# [hnet] >Multiple test correction using holm
# [hnet] >Fin
# Make scatterplot and show the top n_feat enriched features per cluster.
ce.scatter(n_feat=2)
图 4. 带有统计显著特征的聚类标签散点图。图片由作者提供。
最后的话。
了解哪些特征驱动了聚类的形成对于从复杂数据集中提取有价值的见解至关重要。通过特征值为聚类上色的可视检查在处理具有大量特征、不同大小和密度的大型数据集时可能劳动密集且具有挑战性。clusteval库提供了一种定量方法,通过使用超几何检验和 Mann-Whitney U 检验分别对类别变量和连续变量与聚类标签之间的关联进行统计测试,从而评估聚类背后的驱动特征。
一个重要但具有挑战性的步骤是通过适当的数据标准化、距离度量选择和聚类评估来确保聚类的可信度。只有这样,聚类背后的驱动特征才能提供合理的信息。在线购物者意图的示例数据集展示了clusteval在识别聚类背后驱动特征方面的实际应用。总体而言,在聚类分析中结合定量方法来确定驱动特征可以极大地增强复杂数据集的可解释性和价值。
保持安全,保持冷静。
干杯 E.
如果你喜欢这篇关于聚类的博客,欢迎 关注我 以便随时了解我的最新内容,因为我写了更多类似的博客。如果你使用我的推荐链接,你可以支持我的工作,并且可以无限制地访问所有 Medium 博客。
软件
让我们联系吧!
参考资料
-
E. Taskesen,从数据到聚类:你的聚类足够好吗?,2023 年 7 月 Medium。
-
E. Taskesen,通过重要关联网络探索和理解你的数据,2021 年 8 月 Medium
从数据工程到提示工程
使用 ChatGPT 解决数据准备任务
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 5 月 22 日
--
照片由 Ricardo Gomez Angel 提供,来源于 Unsplash
数据工程是数据科学过程中的一个重要部分。在 CRISP-DM 方法中,这个过程阶段被称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在我们的文章中,我们使用 ChatGPT 和 Python 解决典型的数据工程任务。通过这样做,我们探讨了数据工程与新兴领域提示工程之间的联系。
介绍
在 2022 年 5 月,Stephen Wolfram 和 Lex Fridman 进行了题为“编程已死?”的深刻讲座。他们讨论了高层语言是否仍会被开发者使用。根据 Wolfram 的说法,许多编程任务可以用大型语言模型(LLMs)来自动化。在撰写本文时,这种模型的最突出例子是ChatGPT。自 2022 年末推出以来,它产生了惊人的结果。指定 LLM 要执行的操作被称为“提示工程”。如果 Wolfram 是对的,那么软件开发的部分工作将从编写代码转向编写提示。
在数据科学中,数据准备可能是一个耗时且繁琐的任务。那么为什么不尝试用 LLM 自动化呢?在接下来的部分,我们将用 ChatGPT 和 Python 处理不同的数据工程问题。我们没有自己编写 Python 代码,而是使用提示工程来生成代码。我们的实验于 2023 年 5 月 19 日进行,基于当时最新的免费 ChatGPT 版本(GPT-3.5)。
创建数据框
让我们从一个简单的问题开始,并从样本数据集中创建一个 Pandas 数据框。表格 1 包含了由世界银行提供的国家指标。
| Continent | Country | Year | Category | Variable | Value |
|---------------|----------|-------|---------------|-------------|--------|
| Asia | China | 2022 | Economy | GDP | 18.1 |
| Asia | China | 2022 | Demographics | Population | 1412 |
| North America | USA | 2022 | Economy | GDP | 25.5 |
| North America | USA | 2022 | Demographics | Population | 333 |
| Europe | Germany | 2022 | Economy | GDP | 3.9 |
| Europe | Germany | 2022 | Demographics | Population | 84 |
表格 1:包含国家指标的数据集
为了从上述样本创建数据框,我们开始了与 ChatGPT 的新对话,并发布了以下提示:
Create a Pandas table with the following data:
Continent, Country, Year, Category, Variable, Value
Asia, China, 2022, Economy, GDP, 18.1
Asia, China, 2022, Demographics, Population, 1412
North America, USA, 2022, Economy, GDP, 25.5
North America, USA, 2022, Demographics, Population, 333
Europe, Germany, 2022, Economy, GDP, 3.9
Europe, Germany, 2022, Demographics, Population, 84
提示 1:创建数据框
作为回应,模型生成了以下 Python 代码:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
df = pd.DataFrame(data)
print(df)
响应 1:数据框创建代码
运行脚本时,我们看到输出符合我们的规范:
Continent Country Year Category Variable Value
0 Asia China 2022 Economy GDP 18.1
1 Asia China 2022 Demographics Population 1412.0
2 North America USA 2022 Economy GDP 25.5
3 North America USA 2022 Demographics Population 333.0
4 Europe Germany 2022 Economy GDP 3.9
5 Europe Germany 2022 Demographics Population 84.0
即使我们使用了一个样本进行实验,接下来的代码原则上也可以处理更大的数据集。
数据框的透视
对于数据科学而言,我们用第一个提示创建的表格结构并不理想。相反,所谓的“平面表”会更方便。在表格 1 中,指标“GDP”和“Population”由名为“Variable”和“Value”的两列表示。相比之下,平面表将包含每个指标的单独列,如表格 2 所示。
| Continent | Country | Year | GDP | Population |
|---------------|----------|-------|-------|-------------|
| Asia | China | 2022 | 18.1 | 1412 |
| North America | USA | 2022 | 25.5 | 333 |
| Europe | Germany | 2022 | 3.9 | 84 |
表格 2:包含国家指标的平面表
将表格 1 的格式转换为表格 2 的格式称为“透视”。让我们尝试用 ChatGPT 解决这个任务。为此,我们在对话中发布了第二个提示:
Transform the table into the following format:
Continent, Country, Year, GDP, Population
Asia, China, 2022, 18.1, 1412
North America, USA, 2022, 25.5, 333
Europe, Germany, 2022, 3.9, 84
提示 2:数据框透视
上述信息基于所需数据格式的示例。我们本可以用纯口头方式表述我们的请求。与经典编程一样,提示工程中有替代解决方案来实现目标。找到最佳方式将是未来令人兴奋的任务。作为我们提示的结果,ChatGPT 调整了 Python 代码如下:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
df = pd.DataFrame(data)
# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Category', values='Value').reset_index()
# Rename the columns
pivot_df.columns.name = None
pivot_df.columns = [column if column != 'Variable' else None for column in pivot_df.columns]
print(pivot_df)
响应 2:透视代码
然而,运行脚本的第二个版本显示的输出并不是我们所请求的:
Continent Country Year Demographics Economy
0 Asia China 2022 1412.0 18.1
1 Europe Germany 2022 84.0 3.9
2 North America USA 2022 333.0 25.5
而不是预期的列“GDP”和“人口”,表中包含两列名为“人口统计”和“经济”。为了解决这个问题,我们发布了下一个提示:
The columns are not correct, you should have used
the variables GDP and population
提示 3:修复枢轴代码
对此,模型纠正了错误并生成了 Python 代码的另一版本:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
df = pd.DataFrame(data)
# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()
# Rename the columns
pivot_df.columns.name = None
pivot_df.columns = ['Continent', 'Country', 'Year', 'GDP', 'Population']
print(pivot_df)
响应 3:修正的枢轴代码
运行脚本显示,这次实施了正确的枢轴:
Continent Country Year GDP Population
0 Asia China 2022 18.1 1412.0
1 Europe Germany 2022 3.9 84.0
2 North America USA 2022 25.5 333.0
丰富数据框
另一个典型的数据工程任务是用额外信息丰富数据集。为此目的,我们要求 ChatGPT 通过发布以下提示为我们的数据添加ISO 国家代码:
For each country, in the table above, add its ISO code
提示 4:添加国家代码
结果,模型调整了响应并返回了以下代码片段:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}
df = pd.DataFrame(data)
# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)
# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()
# Rename the columns
pivot_df.columns.name = None
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population']
print(pivot_df)
响应 4:带有国家代码的数据框
运行脚本显示,数据框已添加了 ISO 代码的新列。
Continent Country ISO Code Year GDP Population
0 Asia China CN 2022 18.1 1412.0
1 Europe Germany DE 2022 3.9 84.0
2 North America USA US 2022 25.5 333.0
派生一个列
让我们接下来通过将一个国家的 GDP 除以其人口大小来派生一个新列“人均 GDP”。为此,我们发送了这个提示:
Derive a new column "GDP per capita" by dividing the GDP
of a country by its population size
提示 5:派生一个新列
对此,模型修改了代码如下:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}
df = pd.DataFrame(data)
# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)
# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()
# Calculate GDP per capita
pivot_df['GDP per capita'] = pivot_df['GDP'] / pivot_df['Population']
# Rename the columns
pivot_df.columns.name = None
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population', 'GDP per capita']
print(pivot_df)
响应 5:带有新列的数据框
输出显示脚本产生了预期的结果:
Continent Country ISO Code Year GDP Population GDP per capita
0 Asia China CN 2022 18.1 1412.0 0.012819
1 Europe Germany DE 2022 3.9 84.0 0.046429
2 North America USA US 2022 25.5 333.0 0.076577
分析数据
一旦数据转换完成,实际分析就可以开始了。最后让我们计算国家的 GDP 和人口之间的相关系数。为此,我们发布了最后一个提示:
Use the table above to calculate the correlation coefficient between
the GDP and the population of the countries
提示 6:分析相关性
因此,模型返回了以下代码片段:
import pandas as pd
data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}
df = pd.DataFrame(data)
# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()
# Calculate correlation coefficient
correlation_coefficient = pivot_df['GDP'].corr(pivot_df['Population'])
print("Correlation Coefficient: {:.3f}".format(correlation_coefficient))
响应 6:分析相关性的代码
运行脚本时,我们发现这两个变量之间存在正相关,这并不令人意外:
Correlation Coefficient: 0.350
结论
诚然,我们的实验基于一个简单的数据集。尽管如此,结果令人瞩目。我们完成了多项数据工程任务,而不用写一行代码。ChatGPT 不仅在大多数情况下能够正确实现我们的提示,即使在模型犯错时,它也能够反思并修正。正如在软件开发中一样,生成的代码必须经过测试。此外,它可能需要重构和优化。在 AI 时代,使用 pylint 仍然是个好主意。总结来说,然而,我们不得不同意沃尔夫勒姆的观点:未来,数据工程的重要部分将从编码转向提示工程。这种新方法不会取代数据工程师,但会让他们变得更高效。
所有图片(除非另有说明)均由作者提供。
关于作者
克里斯蒂安·科赫是 BWI GmbH 的企业架构师,同时也是纽伦堡理工大学乔治·西蒙·奥姆学院的讲师。
马库斯·施塔迪是 Dehn SE 的高级云数据工程师,多年来一直从事数据工程、数据科学和数据分析工作。
卢卡斯·伯勒 是 TeamBank AG 的数据架构师,专注于设计和实施强大的数据分析架构。
从数据湖到数据网:最新企业数据架构指南
了解为什么大型公司正在接受数据网
·发表于Towards Data Science ·阅读时间 17 分钟·2023 年 5 月 30 日
--
图片来源:takahiro takuchi (Unsplash)
全球大型组织正在经历重大‘数据地震’。
这是公司数据湖向数据网的去中心化。
在我曾经在澳大利亚‘四大’银行之一从事分析工作的半个十年中,我们正处于一次巨大的转型旅程中,同时构建多个重要基础设施项目:
不过我得承认……这对我们可怜的老数据分析师和数据科学家来说有点破坏性。
想象一下在房间装修的时候尝试做书法。
更新:我现在在YouTube发布分析内容。
数据湖——数据科学家喜欢从中“饮水”的热门目的地——目前正像面团一样被拆解,并作为网的一部分被分配到各个业务领域。
一些数据科学家带着好奇心观看;其他人则因沮丧而叹息;还有些人则激动得难以置信。
为什么?
因为数据网格承诺成为一个真正可扩展的数据平台,其中数据被视为一等公民。数据科学家将可以访问可发现、可靠且可重用的数据资产,这些资产可以在公司内不同的业务领域之间无缝共享。
正如人们所说的,短期痛苦换取长期收益。
在这篇文章中,我将深入探讨如何……
-
数据湖成为了瓶颈;
-
为什么组织现在去中心化他们的数据湖,转向数据网格;
-
如何在公司中构建一个网状基础设施。
1. 数据湖的简史
企业数据环境在过去十年里发展得非常快。
在 2010 年代中期,数据湖在全球范围内开始流行。这个概念实际上已经存在了几十年,但在这一时期,构建这些集中式数据存储庞物所需的技术才变得可行。
时机也相当好。
智能手机、物联网(IoT)、数字和社交媒体以及电子商务的爆炸式增长促成了大数据的兴起,随着这一趋势,组织迫切需要存储大量非结构化数据,并利用数据分析和机器学习从中提取见解。
数据湖提供了一种可扩展和灵活的解决方案,无需预定义模式,不同于数据仓库。
数据科学、机器学习和大数据的交集。作者图片
那么运行在这些之上的软件呢?
进入Apache Hadoop,一个开源框架,提供了分布式存储(HDFS)和分布式计算(MapReduce)功能,用于处理湖中的大数据。
Hadoop 的起源可以追溯到 2000 年代初期,Yahoo! 研究人员发表的一对开创性 论文。到 2010 年,Facebook 自豪地拥有全球最大的 Hadoop 集群,储存量达 21 PB。
几年后,半数《财富》50 强公司已经采用了这一框架。
Hadoop 通过使用廉价的消费级机器(商品硬件)和大数据处理能力,实现了成本效益高的存储,这使其成为全球寻求构建数据湖的组织的有吸引力的选择。
自 2010 年代中期以来,基于大数据的预测模型迅猛增长。图像来源于作者
接下来是云计算,它在 2015 到 2020 年间 迅速崛起。
像Amazon Web Services (AWS)、Microsoft Azure 和 Google Cloud Platform (GCP) 这样的巨头提供了可扩展的存储解决方案,如 Amazon S3、Azure Data Lake Storage 和 Google Cloud Storage。
结果是,许多组织将其本地的数据湖迁移到云上,使它们能够灵活适应变化的工作负载,随心所欲地扩展和收缩,只为实际使用的部分付费。
微软现在甚至提供 SaaS 云分析平台,将数据仓库、大数据、数据工程和数据管理 汇聚在一个平台下。
这不是美好时光吗?
嗯……
2. 数据湖怪兽
架构师和数据网格发明者 Zhamek Dehghani 浓缩了企业数据平台的历史为三个阶段:
第一代:专有企业数据仓库和商业智能平台;这些解决方案价格昂贵,使得公司背负了大量技术债务,涉及成千上万无法维护的 ETL 任务,以及只有少数专业人员理解的表格和报告,导致对业务的积极影响未能完全实现。
第二代:以数据湖为银弹的大数据生态系统;由中心团队的超专业数据工程师操作的复杂大数据生态系统和长期运行的批处理作业创造了数据湖怪兽,这些怪兽在最好的情况下仅能支持少量的研发分析;承诺过高而实现不足。
第三代:或多或少与上一代相似,但现代化地向实时数据可用性进行流式处理,统一批处理和流处理以进行数据转换,并完全拥抱基于云的托管服务来处理存储、数据管道执行引擎和机器学习平台。
她的言辞确实很直白。
数十年的数据仓库使组织 沉溺于一个由混乱数据管道连接的数据系统的海洋中。魔法解决方案原本是将数据集中到一个中央存储库中。不幸的是,数据湖梦想在许多组织中变成了数据湖的沼泽。
充满了大量未被利用的数据和未解决的数据质量问题,水变得陈旧。
过度集中任何事物可能会打开一个麻烦缸,通常在一段动荡时期后,会导致剧烈的回归去中心化。我们在各个方面的人类社会中都可以看到这一点:
企业数据经历了其数据湖实验形式的过度集中化。
这是 Dehghani 描述的后果,称之为‘失败模式’。哎呦。
问题 1 — 集中化的难题!
我们得到一个集中化的 领域无关的多面手数据平台,由一个过度劳累的集中化数据团队管理,他们并不是他们处理的数据的专家。
这个高超的数据工程团队被期望从所有企业角落和所有业务领域中摄取操作和交易数据。
他们还需要清洗、丰富和转换数据,以满足多样化的消费者的需求,如 仪表板 和 报告的数据分析师,以及 建模的数据科学家。
哎呀!这要求太高了。
数据湖的 ETL 管道。这些管道由一个集中化的数据团队构建。来源:Z. Dehghani 在 MartinFowler.com(已获许可)
随着全球组织竞相成为数据驱动的组织以保持竞争力,处理来自多个业务领域决策者的所有分析问题的需求落在了这个集中化团队的灵活性上。
这成为了一个重大问题。
这些倒霉的数据工程师没有时间。
对操作数据库进行调整后,留下了一条通往集中化数据湖的破损 ETL 管道。
团队面临不断的管道待修补,几乎没有时间专注于理解他们从整个组织中提取或推送给数据科学家的特定领域数据。
中央数据团队成为了瓶颈。来源: 数据网格架构(经许可使用)
结果——在一些初步的快速成功之后,全球的中央数据团队遇到了巨大的可扩展性问题,成为组织敏捷性的瓶颈。
总结来说——就像一场狂野的派对,数据仓库像五彩纸屑一样传播技术债务,让每个人及其宠物仓鼠创建数据管道。
然后数据湖抢占了风头,通过将数据管道的创建压缩到公司内的一个单点,策划了一场盛大的集中化表演。
现在的公司发现自己不得不通过中央数据团队的小管道来满足整个分析师、数据科学家和经理队伍的无限数据需求。
要点:
也许去中心化数据湖到领域特定团队可能是值得追求的理想位置?
问题 2 — 迟钝的操作模型
Dehghani 描述了第二个问题,即耦合管道分解,其中数据湖……
“……在管道的各个阶段之间有较高的耦合,以交付独立的功能或价值。它是正交分解到变化的轴线的。”
这不容易理解,除非你是一个建筑领域的专家,所以让我用一个个人的例子来说明。
几年前,我工作的银行将整个组织结构从职能操作模型转变为业务线模型。
这是什么意思?我将通过关注抵押贷款销售表现不佳来说明——这是任何传统银行的核心业务。
在职能模型下,有人负责抵押贷款产品开发,有人负责分销和销售,还有人负责法律、风险和合规等事务。
图片由作者提供
这导致了对不佳表现缺乏问责,因为这些职能高度耦合,需要这些跨职能团队彼此协作,以交付端到端的抵押贷款产品。
换句话说,变化的轴线是产品的方向:抵押贷款、信用卡、商业贷款等,但工作管道却是正交分解的,将每个产品切分为一堆高度耦合的职能。
很糟糕,很糟糕,很糟糕。
在新的业务部门模型下,一位高级主管负责抵押贷款业务。一位高级主管负责信用卡。一位高级主管负责商业贷款。
图片由作者提供
现在,管道与变化的轴线平行。方向舵和船只现在已经对齐。
这驱动了:
-
问责制 — 如果抵押贷款表现不佳,C-Suite 先生没有奖金。
-
灵活性 — 抵押贷款业务可以迅速围绕新项目进行重组和创新。变化更容易、更快。
-
性能 — 灵活性和创新带来更快交付的更好产品和服务。
回到数据湖。
结果是,他们也本质上使用了一种功能性的方法来组织工作,其中数据管道被分解为处理阶段,如 源头、摄取、处理 和 服务 数据。
数据管道中的处理阶段。来源:Z. Dehghani 于 MartinFowler.com(已授权)
就像我的银行例子一样,这些处理阶段是高度耦合的。
那么,正在构建信用评分模型的数据科学团队是否突然需要不同的数据?这意味着需要处理不同的数据,这可能意味着需要获取和摄取新的数据。
这意味着我们集中化的数据团队需要管理大量不断演变的依赖关系,从而导致数据交付变慢且缺乏灵活性。
实际上,这些依赖关系意味着整个数据湖中的管道是最小的变更单元,必须进行修改以适应新功能。
因此,我们说数据湖是单体数据平台。
这实际上是一个难以更改和升级的大块内容,正如 Dehghani 主张:
“…限制了我们在响应新消费者或数据源时实现更高速度和规模的能力。”
要点:
我们是否可以通过将数据湖去中心化为更模块化的架构来解决这个问题,并让业务领域对其数据承担端到端责任?
问题 3 — 围栏投掷
Dehghani 称第三种也是最后一种失败模式为隔离和超专业化的所有权,我认为这导致了无效的围栏投掷。
我们在数据湖工作的超专业大数据工程师与数据的来源和消费地点组织上是隔离的。
隔离的超专业化数据平台团队。来源:Z. Dehghani 于 MartinFowler.com(已授权)
这创造了一个不良的激励结构,无法促进良好的交付成果。Dehghani 表达为……
“我个人不羡慕数据平台工程师的生活。他们需要从没有提供有意义、真实和正确数据的团队那里获取数据。这些团队对生成数据的源领域了解甚少,缺乏领域专业知识。他们需要为多样化的需求(无论是操作性的还是分析性的)提供数据,但对数据的应用和访问消费领域专家了解不多。
我们发现的是断开的源团队,沮丧的消费者争夺数据平台团队待办事项上的位置,以及过度扩展的数据平台团队。”
数据生产者将‘打包’一些数据,并将其抛给数据工程师。
现在是你们的问题了!祝大家好运!
超负荷的数据显示,数据工程师可能没有公正地处理摄取的数据,因为他们不是数据领域专家,他们会将一些处理过的数据从数据湖中抛出,以服务下游消费者。
祝好运,分析师和数据科学家!是时候小憩一下,然后我将去修复我待办事项上的五十个破损的 ETL 管道。
从问题 2 和 3 可以看出,数据湖实验中出现的挑战既是组织性的也是技术性的。
要点:
通过将数据管理下放到各个业务领域,也许我们可以促进数据所有权和协作的文化,并赋予数据生产者、工程师和消费者共同合作的能力。
对了,我们能否让这些领域真正参与进来?
通过激励他们以数据作为热销产品对待,赋予他们为建立战略数据资产感到自豪的动力。
喜欢这个故事吗?当我发布类似文章时,请获取一个邮件。
3. 介绍……数据网格!
在 2019 年,Dehghani 提出了数据网格作为下一代数据架构,倡导去中心化的数据管理方法。
她的初始文章——这里和这里——在企业数据社区中引起了广泛关注,这促使许多全球组织开始了自己的数据网格之旅,包括我的。
数据网格不是将数据泵送到集中式数据湖,而是将数据所有权和处理下放到特定领域的团队,这些团队控制和交付数据产品,促进数据在整个组织中的易访问性和互联互通,加快决策速度并促进创新。
数据网格概述。来源:数据网格架构(经许可)
数据网格的梦想是创建一个基础,以规模化提取分析数据的价值,规模化的应用包括:
-
不断变化的业务、数据和技术环境。
-
数据生产者和消费者的增长。
-
多样化的数据处理需求。各种使用案例要求多样化的工具进行转换和处理。例如,实时异常检测可能利用Apache Kafka;客户支持的 NLP 系统常常导致在像NLTK这样的 Python 包上进行数据科学原型设计,图像识别利用像TensorFlow和PyTorch这样的深度学习框架;我银行的欺诈检测团队希望使用Apache Spark处理我们的大数据。
所有这些需求都为数据仓库带来了技术债务(表现为大量无法维护的 ETL 作业),并且对数据湖造成了瓶颈(由于大量的多样化工作被挤压到一个小型集中数据团队中)。
组织最终会面临技术债务超过提供的价值的复杂性门槛。
这是一种糟糕的情况。
为了解决这些问题,Dehghani 提出了四项原则,任何数据网格的实施都必须体现这些原则,以实现规模、质量和可用性的承诺。
数据网格的四项原则。来源:数据网格架构(已获许可)
-
数据的领域所有权: 通过将数据所有权交给领域特定的团队,你赋能于离数据最近的人来负责。这种方法提高了应对业务需求变化的敏捷性,并增强了利用数据驱动洞察的有效性,最终导致更好、更具创新性的产品和服务,且速度更快。
-
数据即产品: 每个业务单元或领域都被赋予产品思维,以设计、拥有和改进高质量且可重用的数据产品——由数据的生产者视为产品的自包含且可访问的数据集。目标是发布和共享数据产品给其他领域的消费者——被视为网格上的节点——以便所有人都能利用这些战略数据资产。
-
自助数据平台: 赋能用户自助能力为加速数据访问和探索铺平道路。通过提供一个用户友好的平台,配备必要的工具、资源和服务,你使团队能够在数据需求方面变得自给自足。数据的民主化促进了更快的决策制定和数据驱动的卓越文化。
-
联合治理: 集中控制扼杀了创新并阻碍了敏捷性。联合的方法确保决策权分布在各个团队中,使他们在关键时刻能够自主决策。通过在控制与自主之间取得正确的平衡,你可以促进问责、协作和创新。
四个原则中,数据产品是最关键的。因此,我们常常看到公司同时执行他们的数据产品战略,并在各个业务领域分散他们的数据湖。
阅读我的解释 101,了解有关数据产品的所有详细信息。
关于执行数据网战略的话题…
4. 如何构建数据网
对于大多数公司来说,这段旅程不会是干净整洁的。
构建数据网不会是一个被 relegated 给一个隔离的工程团队在地下室里辛苦工作的任务,直到准备好部署。
你可能需要巧妙地逐步联合你现有的数据湖,直到你达到一个‘足够的数据网’的平台。
想象在飞行中将两个飞机引擎换成四个较小的引擎,而不是在一个阴凉的机库里建造一架新飞机。
或者尝试在保持部分车道通行的同时升级道路,而不是在附近铺设一条新的道路并在一切就绪时剪彩。
构建数据网是一个重大的任务,你需要在所有阶段让业务共同参与。因为最终将由业务领域来管理自己的端到端数据事务!
完全的数据网成熟可能需要很长时间,因为数据网主要是一种组织构造。
它同样涉及运营模型——换句话说,人员和流程——,以及技术本身,这意味着文化提升和带领人们一起前行是至关重要的。
你需要教会组织数据网的价值以及如何使用它。
正确处理你的策略,随着时间的推移,你的集中式领域无关的单体数据湖将转变为去中心化领域导向的模块化数据网。
设计阶段的一些考虑因素。查看 datamesh-architecture.com 以深入了解。
-
领域。 数据网格架构由一组业务领域组成,每个领域都有一个领域数据团队,能够自行进行跨领域的数据分析。一个支持团队——通常是组织的转型办公室的一部分——在整个组织中推广网格的理念,并作为倡导者。他们以咨询的方式帮助各个领域在成为数据网格的“完整成员”的过程中。支持团队将由数据架构、数据分析、数据工程和数据治理方面的专家组成。
-
数据产品。 各领域将摄取自己操作的数据——这些数据离他们很近,他们对此非常了解——并构建作为数据产品的数据分析模型,这些模型可以发布到网络上。数据产品由领域所有,领域负责其运营、质量和在整个生命周期中的提升。有效的问责制确保数据的有效性。
数据产品在网格上的共享。来源:数据网格架构(经许可使用)
-
自助服务。 还记得学校里的‘多文化食物日’吗?那天每个人都带来自己美味的菜肴,并在自助餐桌上共享?老师的简约角色是监督操作,确保一切顺利进行。同样,网格中新精简的中央数据团队致力于提供和维护一个领域无关的多样数据产品的‘自助餐桌’,供大家自取。业务团队可以进行自己的分析,开销较小,并将自己的数据产品提供给同行。这是一场美味的数据盛宴,每个人也都可以是厨师。
-
联邦治理。每个领域将自行管理自己的数据,并有权按照自己的节奏行动——就像欧盟成员国一样。在某些方面,如果有必要团结和标准化,他们将与其他领域在联邦治理小组中达成协议,制定全球政策,如文档标准、互操作性和安全性——就像欧洲议会一样——以便各个领域能够轻松发现、理解、使用和整合在网格上可用的数据产品。
这里有激动人心的一点——我们的网格什么时候会达到成熟?
当团队开始使用其他领域的数据产品时,网格就会显现出来。
这作为一个有用的基准,旨在证明你的数据网格旅程已经达到了一个成熟的门槛水平。
这是庆祝的好时机!
5. 最后的话
数据网格是一个相对较新的概念,约在 2018 年由架构师扎梅克·德赫戈尼发明。
随着越来越多的组织应对集中式数据湖的扩展性问题,这一理念在数据架构和分析社区中获得了显著的关注。
通过摆脱 由单一团队控制数据的组织结构,转向由使用数据最多的团队拥有和管理数据的去中心化模型,组织的不同部分可以独立工作——拥有更大的自主权和敏捷性——同时仍然确保数据的一致性、可靠性和良好治理。
数据网格提倡一种责任、所有权和协作的文化,其中 数据被产品化并被视为一等公民,以无缝且受控的方式在公司内部自豪地共享。
目标是实现一个真正可扩展且灵活的数据架构,以符合现代组织的数据中心驱动业务价值和创新的需求。
总结数据网格的四个原则。来源:Z. Dehghani 于 MartinFowler.com(已获许可)
我公司自身的历程 预计需要几年时间完成主要迁移,完全成熟则需要更长时间。
我们正在同时处理三个主要部分:
-
云。 从我们在 Microsoft Azure 上的 Cloudera 堆栈的 IaaS 升级到 Azure PaaS 的原生云服务。更多信息请参见这里。
-
数据产品。 初始的一系列基础数据产品正在推出,可以像 Lego 积木一样在不同组合中使用和重新组合,形成更大、更有价值的数据产品。
-
网格。 我们将数据湖去中心化,目标是至少五个节点。
这真是一段奇妙的旅程。当我五年前开始时,我们才刚刚开始利用 Apache Hadoop 在本地基础设施上构建数据湖。
无数的挑战和宝贵的经验塑造了我们的旅程。
像任何决心坚定的团队一样,我们迅速失败并从失败中前进。五年短暂的时间里,我们已经完全转变了企业数据环境。
谁知道五年后的情况会是什么样子?我期待着。
在 Twitter 和 YouTube 这里、这里 以及 这里 找到我。
我的人气 AI、ML 和数据科学文章
-
AI 和机器学习:快速入门 — 这里
-
机器学习与机械建模 — 这里
-
数据科学:现代数据科学家的新技能 — 这里
-
生成式 AI:大公司如何争相采纳 — 这里
-
ChatGPT 与 GPT-4:OpenAI 如何赢得 NLU 战争 — 这里
-
GenAI 艺术:DALL-E, Midjourney 与 Stable Diffusion 解析 — 这里
-
超越 ChatGPT:寻找真正智能的机器 — 这里
-
现代企业数据战略解析 — 这里
-
从数据仓库和数据湖到数据网格 — 这里
-
从数据湖到数据网格:最新架构指南 — 这里
-
Azure Synapse Analytics 实战:7 个用例解析 — 这里
-
云计算 101:为您的业务利用云计算 — 这里
-
数据仓库与数据建模 — 快速速成课程 — 这里
-
数据产品:为分析建立坚实的基础 — 这里
-
数据民主化:5 种“数据普及”策略 — 这里
-
数据治理:分析师的 5 个常见痛点 — 这里
-
数据讲故事的力量 — 销售故事,而非数据 — 这里
-
数据分析入门:谷歌方法 — 这里
-
Power BI — 从数据建模到惊艳报告 — 这里
-
回归分析:使用 Python 预测房价 — 这里
-
分类:使用 Python 预测员工流失 — 这里
-
Python Jupyter Notebook 与 Dataiku DSS 的对比 — 点击这里
-
浅析流行的机器学习性能指标 — 点击这里
-
在 AWS 上构建 GenAI — 我的首次体验 — 点击这里
-
COVID-19 的数学建模与机器学习 — 点击这里
-
工作的未来:在 AI 时代你的职业安全吗 — 点击这里
从数据平台到机器学习平台
数据/机器学习平台如何演变并支持复杂的 MLOps 实践
·
关注 发表在Towards Data Science · 9 分钟阅读 · 2023 年 10 月 22 日
--
数据/机器学习(Data/ML)一直是我们技术领域中最受欢迎的话题。我想分享一下我对数据/机器学习平台的理解,以及这些平台如何从基础发展到复杂。最后,我会尽力涵盖 MLOps,即管理机器学习项目的原则。
关于我,这里是我的 LinkedIn。
旅程的开始:在线服务 + OLTP + OLAP
在最开始时,数据基础设施可能相当简单。分析查询可能会发送到在线OLTP 数据库的只读副本,或者设置 OLAP 数据库作为数据仓库。
这里是可能的基础设施:
图片来源:作者
只要系统能够满足业务需求,就没有问题。满足我们业务需求的所有系统都是好的系统。如果它们简单,那就更好。
在这个阶段,有多种数据分析方法:
-
仅向 OLTP 数据库的副本节点提交查询。(不推荐)
-
启用 OLTP 数据库的 CDC(变更数据捕捉)并将这些数据导入到 OLAP 数据库。关于 CDC 日志的导入服务选项,你可以根据所选择的 OLAP 数据库来选择。例如,Flink 数据流与 CDC 连接器 是处理此问题的一种方式。许多企业服务提供自己的建议解决方案,例如 Snowpipe 适用于 Snowflake。也建议从副本节点加载数据,以保持主节点的 CPU/IO 带宽用于在线流量。
在这个阶段,ML 任务可能在本地环境中运行。你可以在本地设置一个 Jupyter 笔记本,从 OLAP 数据库加载结构化数据,然后在本地训练 ML 模型。
这种架构的潜在挑战包括但不限于:
-
使用 OLAP 数据库管理非结构化或半结构化数据是困难的。
-
当处理海量数据时,OLAP 可能会出现性能回退。(单个 ETL 任务需要 TB 以上的数据)
-
对各种计算引擎的支持不足,例如 Spark 或 Presto。大多数计算引擎确实支持通过 JDBC 端点连接到 OLAP,但并行处理会受到 JDBC 端点自身的 IO 瓶颈严重限制。
-
存储海量数据在 OLAP 数据库中的成本很高。
你可能已经知道解决此问题的方向。建立一个数据湖!引入数据湖并不一定意味着你需要完全淘汰 OLAP 数据库。公司中有两种系统共存以满足不同用例仍然很常见。
数据湖:存储计算分离 + 写时模式
数据湖允许你持久化非结构化和半结构化数据,并执行按需模式。它允许你通过使用专门的存储解决方案存储大量数据来降低成本,并根据需求启动计算集群。它还允许你通过扩展计算集群轻松管理 TB/PB 数据集。
你的基础设施可能如下所示:
作者提供的图片
这确实是一个过于简化的图示。数据湖的实际实现可能要复杂得多。
现在许多云服务提供商已经有相当成熟的数据湖存储解决方案,例如 AWS S3 和 Azure ADLS。在这些存储解决方案之上仍然需要完成许多任务。例如,应该有一个 Hive metastore 来管理你的表元数据,以及一个 Datahub 提供数据可视性。还有一些具有挑战性的话题,如 数据湖中的精细权限控制 和 数据血缘分析(例如 spline)。
为了最大化你的数据湖的价值和效率,我们应该仔细选择每个数据湖层的文件格式和平均文件大小。
作者提供的图片
一般建议包括:
-
避免小文件:小文件是数据湖中高存储成本和性能差的主要原因之一。
-
在 延迟、压缩 比率 和 性能 之间的平衡:具有文件格式如 Hudi 的低延迟数据湖表可能不会给你最佳的压缩比,而大容量的 ORC 文件高压缩比可能会带来性能噩梦。你可能需要根据表的使用模式、延迟要求和表大小来明智地选择文件格式。
一些成熟的 SaaS/PaaS 提供商如 Databricks 提供了相当不错的数据湖(或现在的 LakeHouse)解决方案。你也可以探索 ByteHouse 以获得统一的大数据分析体验。
在机器学习方面,团队可能会开始在远程环境中探索成熟的机器学习框架,如 Tensorflow 和 Pytorch。此外,训练好的机器学习模型可以部署到生产环境中进行在线模型推理。Tensorflow 和 Pytorch 都提供了服务解决方案,例如 TensorFlow Serving 和 Pytorch Serving。
然而,我们的旅程不会就此停止。我们现在可能面临以下挑战:
-
缺乏实时指标和特征管理,这对在线机器学习模型服务至关重要。
-
缺乏模型性能监控。
让我们进一步提升我们的游戏水平。
实时数据/机器学习基础设施:数据流 + 数据流媒体 + 特征存储 + 指标服务器
通常,构建实时数据基础设施是公司多个部门的共同努力。构建数据流的初衷通常不是为了数据/机器学习系统,而是通过消除同步调用来允许微服务进一步扩展。相反,微服务将通过与像 Kafka 这样的消息中间件进行通信而提高效率(代价是较低的一致性水平)。
整体架构可能如下所示。
作者提供的图片
有了在数据河流(例如Kafka)中的数据,我们可以构建数据流管道来处理实时数据。这些数据可以直接用于在线特征存储或同步到像Pinot这样的度量服务器。度量服务器可以进一步处理/聚合这些度量点,形成更有用的模型性能度量和业务度量。你也可以采用像RisingWave这样的流数据库,它可以用 SQL 语法对流数据进行连接/聚合。
对于数据流构建,Flink相当受欢迎。你也可以使用带有 CDC 连接器的 Flink从 OLTP 数据库提取数据,并将数据发送到消息中间件和数据湖。
应该有一个在线特征存储,支持如ScyllaDB或 AWS Dynamo DB 等键值数据库。在线特征存储可以帮助你用与特定参考 ID(用户 ID、产品 UUID)相关联的特征向量丰富发送给模型服务的请求。这可以极大地解耦构建微服务的后台服务团队与构建 ML 模型的 ML 工程师团队之间的依赖关系。它允许 ML 工程师独立推出新的 ML 特征与新 ML 模型(当你更新特征向量时,你的模型服务 API 签名对微服务的暴露将保持不变)。
作者提供的图片
在书中,设计机器学习系统提到了模型堆叠(Jen Wadkin 的 medium 帖子关于模型堆叠)。在模型服务中使用模型堆叠是相当常见的。当你想将异构模型堆叠在一起时,例如将 pytorch 和 tensorflow 模型堆叠在一起时,需要一个协调器。你还可以通过在路由请求到不同模型时根据模型性能动态调整权重,使你的协调器变得更加复杂。
现在我们有了一个复杂的系统。它看起来很酷,但带来了新的挑战:
-
如果不加以管理,系统的债务将迅速增加。
-
对 ML 工程师而言认知负担很重。
这时你可能需要思考 MLOps 如何帮助你。
MLOps:抽象、可观察性和可扩展性
MLOps 从来不是一个具体的解决方案。它更像是一组用于管理 ML 系统的原则。与典型的软件项目不同,ML 系统受数据变化的影响很大,数据依赖管理并非易事。论文机器学习系统中的隐性技术债务详细描述了这些挑战。因此,一个由 MLOps 驱动的 ML 平台必须能够:
-
数据变化监控和数据质量监控。
-
管理离线和在线环境中的 ML 特征。
-
可重复的 ML 管道,满足实验-操作对称性。
-
简洁的 ML 管道配置,可以抽象出基础设施的细节。
本文,MLOps: 机器学习中的持续交付和自动化管道,强调了实验-操作对称性的重要性。它还描述了 MLOps 的自动化级别,从 level-0、level-1 到最终的 level-2。我非常喜欢这份文档中的图表,将借用它来解释 level-1 MLOps 的样子。
作者提供的图像。描述了MLOps: 机器学习中的持续交付和自动化管道中的 MLOps Level-1。
为了在您的组织中扩展这种 MLOps 实践,您需要提供简洁的 ML 管道配置,以便为 ML 工程师抽象出基础设施实现细节。这样,平台工程师也能在不对平台用户造成过多干扰的情况下灵活地升级 ML 平台。您可以考虑使用像 yaml 这样的配置文件来描述 ML 管道,并依靠您的 ML 管道控制器将其转换为实际的工作负载。
因此,让我们通过以下图表重新组织实时数据/ML 基础设施,以突出 MLOps 如何塑造我们的平台。
作者提供的图像
为了给您更好的了解 ML 管道可能的样子,这里是每个阶段的可能抽象示例。以下图表仅帮助您进一步理解配置的可能样子,并不代表任何实际实现,也不涵盖所有所需的方面。
ML 管道配置的一般概念。作者提供的图像
Kubernetes 是一种流行的解决方案,用于编排机器学习工作负载(或如今的所有工作负载)。你可以使用 CRD 提供用户与平台之间的简洁接口。在文章我对 Kubebuilder 的思考中,我分享了在使用 kubebuilder 构建 CRD 时的一些想法。
图片来源于作者
当然,我没有涵盖许多重要的子主题,包括但不限于:
-
超参数优化
-
分布式训练架构
接下来做什么
你可以看到MLOps 只是给已知任务一个合适的名称。这远不是任务完成。我分享的是一个有观点的机器学习操作平台实施策略。即便如此,创建高质量机器学习产品的门槛仍然很高,收集、处理和挖掘数据的工作量仍然很大。
除了这些挑战之外,我还想分享我观察到的机器学习领域的趋势。鉴于这个领域发展的如此快速,这绝对不是一个完整的列表。
-
无服务:我们将机器学习的价值抛在了很远的后面,因为机器学习平台通常建立在数据平台的基础上。这就像在移动时代强迫用户购买计算机来参与社交媒体平台一样。无服务的数据服务和数据引擎正在应对这个挑战。许多服务提供商探索自己的无服务解决方案,以降低采用门槛,例如Databricks、Snowflake、Bytehouse。公司可以在启动数据仓库、数据湖或湖仓后开始构建他们的机器学习产品。
-
AI 驱动的特征工程:好吧,AI 现在可以做一切,对吗?
-
MaaS 趋势:更强大的模型即服务(Model-as-a-Service)将会出现。公司可以直接利用机器学习的力量,而无需自己构建机器学习服务,从而极大地提升他们的业务。
正如我们都注意到的,机器学习领域发展如此迅速。在我打字的此刻,这篇文章可能已经过时。更多的想法已经出现并转化为现实。请告诉我你对机器学习操作的看法,或者我应该在哪些方面进一步学习。让我们一起保持进度!
从数据到聚类:你的聚类何时足够好?
原文:
towardsdatascience.com/from-data-to-clusters-when-is-your-clustering-good-enough-5895440a978a
通过聚类方法可以发现隐藏的宝石,但你需要正确的聚类方法和评估方法来创建合理的聚类。学习如何在四个步骤中找到它们。
·发表于Towards Data Science ·阅读时间 17 分钟·2023 年 4 月 26 日
--
图片来源:Shubham Dhage在Unsplash
通过无监督聚类分析,我们可以对具有相似模式的观察数据进行分组,揭示数据中的(隐藏)趋势。使用聚类评估方法有助于确定聚类倾向、质量和最佳聚类数。在本博客中,我们将深入探讨聚类评估方法,学习如何解释这些方法,并选择适合你用例的聚类方法。 我们将从探讨聚类和评估方法的基础知识开始,这些方法用于评估聚类的质量,包括流行的技术,如轮廓系数、Davies-Bouldin 指数和导数法。通过使用玩具示例数据集,我们将研究每种评估方法的优缺点,提供关于如何解释其结果的实际见解。所有分析都使用clusteval 库。
无监督聚类。
使用无监督聚类,我们的目标是确定数据中的“自然”或“数据驱动”的组,而不使用有关标签或类别的先验知识。使用不同的无监督聚类方法的挑战在于,它会导致样本的不同划分,因此会有不同的分组,因为每种方法隐式地对数据施加了结构。因此,问题出现了;什么是“好的”聚类? 图 1A 展示了一组在二维空间中的样本。你看到多少个簇? 我会说有两个簇而不使用任何标签信息。为什么? 因为点之间的距离很小,而杂乱点之间的“间隙”相对较大。
图 1。图像由作者提供
考虑到这一点,我们可以将对“簇”的直观理解转化为数学陈述;所谓簇内的样本方差应该很小(簇内方差 σW,红色和蓝色),而同时簇之间的方差应该很大(簇间方差,σB),如图 1B 所示。样本之间的距离(或内在关系)可以用距离度量(例如,欧几里得距离)来测量,并存储在所谓的不相似矩阵中。然后,可以使用连接类型计算样本组之间的距离(用于层次聚类)。
距离度量
最著名的距离度量是欧几里得距离。尽管它在许多方法中被设置为默认度量,但它并不总是最佳选择。各种距离度量的示意图见图 2。
理解距离度量的数学性质,以便它适合数据并与研究问题对齐。
图 2:最受欢迎的距离度量的示意图。图像由作者提供。
连接类型。
层次聚类的过程涉及将样本分组到一个更大的簇中的方法。在这个过程中,需要计算两个子簇之间的距离,不同类型的连接描述了簇之间的连接方式(图 3)。
图 3:连接类型。图像由作者提供。
简而言之,两个簇之间的单链接是它们两个最接近样本之间的距离。它产生一个长链,因此适用于异常检测或蛇形簇的聚类。两个簇之间的完全链接是它们两个最远样本之间的距离。直观地说,两个最远的样本之间的相似度不可能比其他相似度较低的样本对更大。这迫使簇变得球形,并且通常在边界处有“紧凑”的轮廓,但内部不一定紧凑。两个簇之间的平均链接是一个簇中所有对象与另一个簇中所有对象之间距离的算术平均值。质心链接是簇的几何质心之间的距离。换句话说,质心链接基于数据点的质心连接簇,而平均链接则考虑了两个簇中所有对象之间的距离。选择度量标准和链接类型时要小心,因为它直接影响最终的聚类结果。
从数据到簇的 4 个步骤。
为了确定数据驱动的簇,这些簇可能包含新的或未知的信息,我们需要仔细地执行四个构成步骤,从输入数据集到合理的簇。像distfit和clusteval这样的库可以帮助完成这个过程。
提出合理的样本分组需要的不仅仅是盲目运行聚类算法。
第一步:调查数据的基础分布。
调查数据的基础分布是一个重要的步骤,因为聚类算法依赖于数据的统计特性来识别模式并将相似的数据点分组在一起。通过理解数据的分布,如其均值、方差、偏度和峰度,我们可以做出明智的决定,选择合适的聚类算法以及如何设置其参数以实现最佳结果。此外,调查数据分布可以提供有关在聚类之前应用适当的归一化或缩放技术的见解。虽然监督方法,如树模型,能够处理混合数据集,但聚类算法则设计用于处理同质数据。这意味着所有变量应该具有类似的类型或测量单位。 归一化或缩放是一个重要的步骤,因为聚类算法基于相似性使用度量标准对数据点进行分组。有关调查基础数据分布的更多细节可以在这里阅读 [1]:
了解潜在的数据分布是数据建模的关键步骤,并且有许多应用,例如异常检测……
[towardsdatascience.com
第 2 步:对簇的密度和期望簇大小做出有根据的猜测。
设定对簇密度、形状和簇数量的期望将有助于选择合适的聚类算法和参数设置,以实现期望的结果。此外,设定期望可以在解释和向利益相关者传达聚类结果时提供更多的信心和有效性。例如,如果目标是识别数据集中稀有的异常,并且聚类结果产生了一个非常低密度的小簇,这可能表明存在这样的异常。然而,并非总是可以设定关于簇数量或密度的期望。此时,我们需要根据与数据的统计属性和研究目标相匹配的数学属性选择聚类方法。
第 3 步:选择聚类方法。
选择聚类方法取决于第 1 到第 4 步,但我们还应该考虑可扩展性、鲁棒性和易用性等因素。例如,在生产环境中,我们可能需要与实验用例不同的属性。有几种流行的聚类方法,如 K-means、层次聚类和基于密度的聚类算法,每种方法都有其自身的假设、优点和局限性(见下文总结)。选择聚类方法后,我们可以开始对数据进行聚类并评估其性能。
K-means 假设簇是球形的、大小相等的,并且具有相似的密度。它需要事先指定簇的数量(k)。注意,最佳簇数量的检测在 clusteval 库中会自动进行。
层次聚类通过基于距离或相似性度量递归地合并簇来构建树状结构的簇。它是聚合性的(自底向上)且不需要事先指定簇的数量。
基于密度的聚类算法,例如 DBSCAN(基于密度的空间聚类应用于噪声),将密集的点组在一起,并且在簇之间具有较低的密度。它们不假设簇的任何特定形状或大小,并且可以识别任意形状和大小的簇。基于密度的聚类特别适用于识别不同密度的簇和将异常点检测为噪声。然而,它们需要调整超参数,例如密度阈值,并且对参数选择敏感。
不同的聚类方法可能会导致样本的不同划分,从而产生不同的分组,因为每种方法都隐含地对数据施加了一种结构。
第 4 步:簇评估。
簇评估是为了评估聚类倾向、质量和最佳簇数。有各种聚类评估方法,其中最流行的是clusteval库中包含的,即Silhouette 分数、Davies-Bouldin 指数和导数(或肘部)方法。使用这些技术的难点在于,聚类步骤及其评估通常是交织在一起的,clusteval 库内部处理了这一点。在下一节中,我将深入探讨各种方法并测试它们的表现。
在聚类过程中应进行簇评估,以为每个聚类分配分数,并实现结果之间的有意义比较。
下一步:从簇到洞察。
找到最佳簇数后的步骤是确定簇背后的驱动特征。一个很好的方法是使用HNET[2]进行富集分析,我们可以使用超几何检验和 Mann-Whitney U 检验来测试簇标签与特征之间的显著关联。更多细节可以在这里找到:
探索以理解您的数据可以决定一个项目的成功与否!
towardsdatascience.com
Clusteval 库。
关于clusteval库的几句说明,该库用于所有分析。clusteval 库旨在应对评估簇和创建有意义图表的挑战。在之前的博客[3]中,我展示了它在图像识别中的强大功能,用于图像的聚类。clusteval库包含了最受欢迎的评估方法,即Silhouette 分数、Davies-Bouldin 指数和导数(或肘部)方法,用于评估层次聚类、K-means、DBSCAN 和 HDBSCAN的聚类。它可以处理所有数据类型,例如连续数据、分类数据和离散数据集。只需确保 1. 数据正确归一化。2. 所有变量具有相似的类型或测量单位。3. 选择适当的距离度量和评估方法。
Clusteval 功能
-
它包含了针对轮廓系数、Davies-Bouldin 指数以及用于评估层次聚类、K 均值、DBSCAN 和 HDBSCAN 的导数方法的最受欢迎的集群评估管道。
-
它包含创建有意义图形的功能;集群的最佳数量、带系数的轮廓图、散点图和树状图。
-
它包含了确定集群背后驱动特征的功能。
-
它可以处理所有数据类型,如连续型、类别型和离散数据集。只需确保所有变量具有相似的类型或测量单位。
聚类方法的选择很重要。
不同的集群技术可以导致不同的分组,因为它们对不同的密度和组大小的响应各异。因此,确定正确的集群数量可能具有挑战性,尤其是在高维空间中,当视觉检查不可行时。通过集群评估方法,我们可以进行调查并进行一些回测,以确定最佳的集群数量并描述集群趋势。为了展示如何确定集群数量,我创建了七个玩具示例数据集(灵感来源于scikit-learn),这些数据集具有不同的密度、形状和样本大小,即蛇形集群、不同密度和大小的集群以及均匀分布的样本(图 4)。每个玩具示例包含 1000 个样本和两个特征。因此,它可以在低维空间中工作时提供直观感受,例如在特征提取步骤之后,例如PCA、t-SNE 或 UMAP步骤。
图 4. 七个玩具示例数据集,具有不同的密度、形状和大小。样本根据其实际标签进行着色。图片由作者提供。
对于这七个示例数据,我们将使用在clusteval中可用的评估方法进行集群分析;轮廓系数、Davies-Bouldin 指数和导数(或肘部)方法。集群本身使用K 均值、层次聚类(单一、完全和 Ward 距离)和基于密度的聚类算法(DBSCAN)进行。
如果你觉得这篇文章对你有帮助,请使用我的 推荐链接 继续无缝学习并注册 Medium 会员。此外, 关注我 以保持更新我的最新内容!
使用轮廓系数进行集群评估。
轮廓系数法是一种衡量样本与自身簇(内聚性)相比于其他簇(分离性)的相似度的指标。得分范围在[-1, 1]之间,高值表示样本或观察值与自身簇匹配良好,而与相邻簇匹配较差。这是一种样本级的方法,这意味着对于每个样本,都会计算一个轮廓系数,如果大多数样本的值很高,则认为聚类配置是合适的。如果许多点的值较低或为负,则可能是聚类配置的簇数过多或过少。轮廓系数法与距离度量无关,这使得它成为一种有吸引力且通用的方法。
让我们看看它如何在一个简单的数据集上对具有相似密度和大小的样本进行分组,即blob 数据集。在下面的代码部分,我们将初始化DBSCAN方法与轮廓系数评估方法。请注意,我们可以使用任何其他聚类方法,如下一节所示。对于DBSCAN,在 epsilon 参数上进行搜索,并检索具有最高轮廓系数的簇数量。具有轮廓系数的DBSCAN正确检测出 4 个簇,并且样本级轮廓系数(图 5 中间)看起来稳定(簇的高分且形状均匀)。这是预期的,因为簇中的样本远离其他簇,并且接近各自簇的中心。
# Import library
from clusteval import clusteval
# Initialize
cl = clusteval(cluster='dbscan', evaluate='silhouette', max_clust=10)
# Import example dataset
X, y = cl.import_example(data='blobs', params={'random_state':1})
# find optimal number of clusters
results = cl.fit(X)
# Make plot
cl.plot()
# Show scatterplot with silhouette scores
cl.scatter()
# [clusteval] >INFO> Fit with method=[dbscan], metric=[euclidean], linkage=[ward]
# [clusteval] >INFO> Gridsearch across epsilon..
# [clusteval] >INFO> Evaluate using silhouette..
# [clusteval] >INFO: 100%|██████████| 245/245 [00:07<00:00, 33.88it/s]
# [clusteval] >INFO> Compute dendrogram threshold.
# [clusteval] >INFO> Optimal number clusters detected: [4].
# [clusteval] >INFO> Fin.
# [clusteval] >INFO> Estimated number of n_clusters: 4, average silhouette_score=0.812
图 5. 左侧:优化 Epsilon 参数以检测最佳簇数量。中间:每个样本的轮廓系数。右侧:确定的簇标签。图片由作者提供。
这只是一个数据集和一种聚类方法(DBSCAN)的评估。让我们在七个玩具示例数据集和五种聚类方法中进行迭代,使用轮廓系数方法确定最佳簇的数量。结果如图 6 所示,其中上半部分显示了按最佳簇数量着色的散点图,下半部分显示了不同簇数量的轮廓系数图。
图 6 的结论是如果我们选择适合数据集的聚类方法,那么我们可以很好地检测出正确的簇的数量。这可能看起来是一个微不足道的评论,但选择合适的聚类方法可能具有挑战性,因为你需要仔细执行前面章节中描述的第 1 到第 4 步。
如果我们由于高维度无法直观地检查结果,我们仍然可以获取样本分布和簇形成的直觉。 如果各组明显分离,如蛇形簇和斑点簇,我们可以期待在特定簇数上出现轮廓系数的强峰值,并且使用正确的聚类方法。但当数据变得更嘈杂时,如球形数据集,多重峰值可能会出现,且轮廓系数的斜率可能会逐渐增加而没有明显的峰值。这通常暗示簇的形成不够强烈。通过clusteval中的min_clust
和/或max_clust
参数,我们可以限定搜索空间并确定相对较高的峰值。这个检查和限定的手动优化步骤被称为回测。图 6 中的一个有趣观察是,当数据变得嘈杂时,DBSCAN聚类方法的得分显示出涡流增加(见图 6,第二列)。原因是检测到许多离群点并开始形成更小的簇,后来这些离群点可能再次成为较大簇的一部分。
图 6. 使用轮廓系数方法检测五种聚类方法和七个玩具示例数据集的最佳簇数。图片由作者提供。
使用戴维斯-鲍尔丁指数进行聚类评估。
戴维斯-鲍尔丁指数(DBindex)可以直观地描述为簇内距离与簇间距离的比率度量。得分在[0, 1]之间,其中较低的值表示簇之间更紧密和更好的分离。较低的得分因此被认为更好。 与轮廓系数方法不同,戴维斯-鲍尔丁指数不是逐样本度量,因此使用速度更快。DBindex的一个缺点是它常常过度预估,因为随着簇数增加,较低的值更为常见。这可以通过得分逐渐下降的斜率没有明显低得分来清晰地看到。
让我们看看如何对具有相似密度和大小的样本进行聚类,以获取戴维斯-鲍尔丁指数性能的一些直觉。在下面的代码部分,我们将用戴维斯-鲍尔丁指数评估方法初始化凝聚聚类方法。如果我们在clusteval中使用max_clust=10
限制搜索空间,它能正确检测到 4 个簇(图 7 左侧面板)。然而,如果我们不限制搜索空间,得分的斜率将逐渐下降,从而导致检测到错误的簇数(图 7 右侧面板)。
# Import
from clusteval import clusteval
# Initialize
cl = clusteval(cluster='agglomerative', evaluate='dbindex', max_clust=10)
# Import example dataset
X, y = cl.import_example(data='blobs', params={'random_state':1})
# find optimal number of clusters
results = cl.fit(X)
# Make plot
cl.plot(figsize=(12, 7))
# Show scatterplot with silhouette scores
cl.scatter()
# [clusteval] >INFO> Fit with method=[agglomerative], metric=[euclidean], linkage=[ward]
# [clusteval] >INFO> Evaluate using dbindex.
# [clusteval] >INFO: 100%|██████████| 18/18 [00:00<00:00, 120.32it/s]
# [clusteval] >INFO> Compute dendrogram threshold.
# [clusteval] >INFO> Optimal number clusters detected: [4].
# [clusteval] >INFO> Fin.
图 7. Davies-Bouldin 指数 对不同数量簇的评分。左侧面板:最多 10 个簇。右侧面板:最多 20 个簇。图片由作者提供。
对于Davies-Bouldin 指数方法,我们也将遍历这七个示例数据集,但我将仅包括Agglomeration簇方法的单一、完整和 Ward 连接。请注意,DBSCAN 与Davies-Bouldin 指数不兼容。在所有示例中,搜索空间有一个限制(max_clust=10
),以防止过度估计(图 8)。尽管我们限制了搜索空间,但仍然存在一个逐渐下降的斜率,在许多示例数据集中导致不正确的簇数量。这在蛇形和各向异性簇(前三行)中尤为明显。一种解决方案是进一步降低最大簇数量(max_clust
)。对于Blobs, Globular, 和 Density示例数据集,可以从散点图中看出正确的簇趋势,但最佳簇数量经常过度估计。在这里,如果我们能够进一步将最大簇数量限制为 6 或 7,结果会有所改善。
图 8. 使用Davies-Bouldin 指数方法检测七个示例数据集的最佳簇数量。图片由作者提供。
使用导数方法进行簇评估。
导数方法 比较每个簇合并的高度与平均值,并通过计算前几个层级深度的标准差进行归一化。最后,导数方法返回基于所选层次聚类方法(红色垂直线)的最佳截断的簇标签。换句话说,当蓝色线中的“肘部”很明显时,可以在橙色线中看到一个强峰值。让我们看看导数方法在聚类具有类似密度和大小的样本方面的效果。在下面的代码部分,我们将初始化Agglomerative聚类方法,并使用导数评估方法。与Davies-Bouldin 指数相比,我们不需要限制搜索空间(图 9),可以轻松检测到 4 个簇。
图 9. 导数方法 对不同数量簇的评分。图片由作者提供。
# Import library
from clusteval import clusteval
# Initialize
cl = clusteval(cluster='agglomerative', evaluate='derivative', max_clust=20)
# Import example dataset
X, y = cl.import_example(data='blobs', params={'random_state':1})
# find optimal number of clusters
results = cl.fit(X)
# Make plot
cl.plot(figsize=(12, 8))
# Show scatterplot with silhouette scores
cl.scatter()
我们将遍历这七个示例数据集,并使用导数方法(图 8)确定最佳簇数量。可以看出,导数方法经常导致不正确的簇数量。如果我们仔细观察导数线(橙色),它在许多数据集中显示出一个小峰值,指向一个较弱的肘部,从而可能表示数据噪声。这表明估计的簇数量可能不可靠。
图 9. 使用导数法检测七个玩具示例数据集的最佳聚类数量。图片由作者提供。
请注意,聚类评估方法很容易被欺骗,因为分数可能会随着聚类数量的增加而逐渐提高。
结束语。
我展示了不同的聚类方法如何响应不同的数据集,以及确定最佳聚类数量需要多个步骤。此外,不同的聚类方法将产生不同的分组,因为它们隐式地对数据施加了结构,从而影响样本的划分。clusteval 包 可以帮助进行聚类调查和回测,其结果可以通过各种绘图功能进行探索。
一般来说,K-means 聚类效果很好,特别是对于球形、平衡的和球状聚类。然而,它不适合检测具有不同密度或蛇形聚类的情况。另一方面,DBSCAN 是一个很好的全能选手,可以在大多数情况下检测到正确的聚类数量。只是要注意,聚类倾向可能是正确的,但检测到的确切聚类数量可能不对。这在球状聚类中尤其如此,因为许多(组)样本可能被标记为离群点。DBSCAN 的一个缺点是计算量大,特别是在优化 epsilon 参数时。相反,对于蛇形聚类或离群点检测,使用单链聚类;对于噪声球状聚类,使用 ward 或完全链路,K-means 适用于球形、平衡的和球状聚类。
对于聚类评估方法,轮廓系数有很多优势。它是一种逐样本的度量,可以与任何距离度量一起使用,使其成为一种有吸引力且多用途的方法。另一方面,Davies-Bouldin 指数和导数法需要更多的迭代过程进行回测和调查,因为它们往往超出正确的聚类数量。Davies-Bouldin 指数在处理具有重叠聚类的数据集时被描述为更具鲁棒性,并且计算效率更高,因为它不需要计算所有数据点之间的成对距离。
总结来说,聚类评估方法的选择取决于数据集的特定特征、分析目标和应用要求。你可以使用多种方法进行回测,并比较其结果,以便根据问题的具体背景做出明智的决定。
保持安全,保持警觉。
干杯 E.
如果你觉得这篇文章对你有帮助,请使用我的 推荐链接 继续无限制地学习,并注册 Medium 会员。此外, 关注我 以保持最新内容!
软件
让我们联系!
参考文献
-
E. Taskesen,如何找到最适合你数据的理论分布,2023 年 2 月,Towards Data Science, Medium。
-
E. Taskesen,通过显著关联网络探索和理解你的数据,2021 年 8 月,Towards Data Science, Medium
-
E. Taskesen, 图像聚类的逐步指南,2021 年 12 月,Towards Data Science, Medium。
从数据到市场策略:利用行为分割
原文:
towardsdatascience.com/from-data-to-market-strategy-using-behavior-segmentation-d065da224262
如何为业务增长个性化你的产品
·发表于 Towards Data Science ·4 分钟阅读·2023 年 1 月 10 日
--
图片由 Belinda Fewings 提供,发布在 Unsplash
74%的客户在你的产品服务没有个性化时感到沮丧(来源 Forbes)。行为分割可以帮助解决这个问题。简单来说,行为分割就是了解客户行为——他们如何与您的产品或网站互动,他们的使用频率,他们花费多少,以及他们购买了哪些产品。
一旦你了解了客户行为,就可以个性化你的产品。这可以带来更高的客户满意度以及业务增长。在这篇博客中,你将看到如何利用行为分割从数据到市场策略的过程。
行为分割过程(作者提供的图像)
数据收集和特征创建
行为分割需要从各种来源收集数据,如网站导航数据、账单数据、产品购买数据等。数据越多,你就越能深入理解客户行为。
为了说明这一过程,让我们以一家电信公司为例。这里展示的是在客户层面创建的样本特征。它包含了人口统计信息、客户使用的服务以及账单信息。下图仅显示了 40 多个特征中的部分样本特征。
样本数据(作者提供的图像)
使用聚类进行分段
我们可以使用聚类算法创建分段,并将每个客户分配到一个分段。下面展示的是聚类算法的结果。显示了三种分段,分别用蓝色、红色和绿色表示。每个点对应一个客户。
使用聚类进行分段(图像由作者提供)
分段解释
那么这些分段意味着什么?雷达图可以帮助我们解释这些分段。
例如,绿色聚类包含了那些服务时间最长的客户,他们使用所有服务,如互联网服务、在线备份、技术支持和流媒体电影。
解释绿色聚类(图像由作者提供)
蓝色聚类是服务时间最短、所有服务使用程度适中的客户。
解释蓝色聚类(图像由作者提供)
红色聚类对应的是只有电话服务且没有数字互联网服务的客户。
解释红色聚类(图像由作者提供)
市场策略
一旦解释完分段,下一步就是制定市场策略。
绿色分段是数字互动客户。
一种市场策略可以是创建数字忠诚卡,并根据数字服务的使用情况奖励客户。这也将增加公司的收入。
蓝色分段是互动适中但服务时间较短的客户。
一种市场策略可以是提供折扣并将他们转化为长期合同。
红色分段是只有电话服务的基本客户。
一种市场策略可以是向他们介绍数字服务的优势,然后推销数字产品。
结论
行为分段可以帮助制定个性化的市场策略。在这篇博客中,你看到了开发这种个性化市场策略的过程。
数据源引用
本博客使用的数据集来自于这里提供的电信数据集。允许商业和非商业用途。
编辑描述
community.ibm.com](https://community.ibm.com/community/user/businessanalytics/blogs/steven-macko/2019/07/11/telco-customer-churn-1113?source=post_page-----d065da224262--------------------------------)
请通过我的推荐链接加入 Medium。
[## 通过我的推荐链接加入 Medium - Pranay Dave
阅读 Pranay Dave(以及 Medium 上的其他成千上万的作者)的每一个故事。你的会员费用直接支持……
请订阅以保持更新,每当我发布新故事时您将会知晓
## 每当 Pranay Dave 发布内容时,获取电子邮件通知
每当 Pranay Dave 发布内容时,您将收到一封电子邮件。通过注册,如果您还没有 Medium 账户,系统将为您创建一个……
进一步阅读
请查看我的相关博客。
如何有效利用细分市场实现商业增长
towardsdatascience.com
YouTube 频道
您也可以在我的 YouTube 频道观看有关行为细分的视频
网站
您可以访问我的网站,这是一个无代码平台,用于学习数据科学。https://experiencedatascience.com
从数据到营销策略,通过事务性细分
原文:
towardsdatascience.com/from-data-to-marketing-strategy-using-transactional-segmentation-640b527a677b
如何有效利用细分实现业务增长
·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 5 日
--
创建于 openai.com labs.openai.com/s/SpAZlVi8fVRXueTBoKdi9p0w
大公司拥有数百万客户。如果没有细分,很难制定营销策略或进行客户沟通,如电子邮件或新闻通讯。客户细分是企业优化营销和产品开发工作、以及更好服务客户的重要工具。
有多种方式来细分客户。在本博客中,我将重点介绍事务性客户细分,也称为 RFM(Recency, Frequency, Monetary)建模。
RFM 是
R = 最近一次购买 = 客户上次购买的时间
F = 频率 = 客户购买的频率
M = 金额 = 购买的金额
这里展示了使用事务性细分创建营销策略的过程。
事务性建模过程(图像由作者提供)
购买交易
为了演示事务性细分,我将以一个在线零售商为例。这里展示了零售交易的样本数据。
零售交易样本数据(图像由作者提供)
数据包含信息,如发票号码、购买的产品、购买的数量以及价格。
RFM 模型
RFM 计算是在客户层面进行的。发票日期可以用来计算最近一次购买,发票号码可以用来计算购买频率,总价可以用来计算金额值。
在 RFM 计算之后,数据将包含自上次购买以来的天数、按月计算的发票频率以及作为一个月所有购买总额的货币价值。
这里展示了一个 RFM 计算的示例数据。
针对每个客户的 RFM 计算(图片由作者提供)
聚类
RFM 计算针对每个客户进行,时间范围可以是每月、每年或业务所需的其他时间范围。使用 RFM 值,可以通过聚类算法对客户进行细分。
这里展示的是聚类算法的结果。每个点对应一个客户。点的颜色对应于细分市场。总共有 5 个细分市场,每个客户都被分配到一个细分市场中。
基于 RFM 数据的聚类(图片由作者提供)
分段解释
拥有一个美观的聚类可视化是很好的。然而,解释这些集群的含义同样重要。
可以帮助进行分段解释的可视化方法是平行坐标,如下所示。可以看到关于最近购买、购买频率和货币价值的垂直线。所有值都已标准化为 0 到 100 之间。最右侧的垂直线用于表示各个细分市场或集群。
使用平行坐标可视化进行解释(图片由作者提供)
营销策略
使用平行坐标可视化,我们可以解释每个集群并制定营销策略。这里展示的是每个集群或细分市场的可视化。
解释每个集群(图片由作者提供)
我们现在可以解释每个集群并制定策略。
集群 0
-
解释 — 最近未进行购买的客户
-
策略 — 提供优惠以促使他们回归购买
集群 1
-
解释 — 具有高货币价值的客户
-
策略 — 创建忠诚度计划,以便他们能够继续增加消费
集群 2
-
解释 — 最近未进行购买的客户
-
策略 — 提供优惠以促使他们回归购买
集群 3
-
解释 — 可能会流失的客户
-
策略 — 通过激动人心的优惠来留住他们
集群 4
-
解释 — 常客
-
策略 — 创建忠诚度计划,以保持他们定期购买
结论
在这篇博客中,你看到了如何从数据到创建营销策略以促进业务增长。使用 RFM 进行客户细分是一种非常有效的策略,可以通过聚类和解释可视化来实施。
数据源引用
博客中使用的数据源可以在这里找到
archive.ics.uci.edu/ml/datasets/online+retail
可以用于商业或非商业目的,引用格式如下
Daqing Chen, Sai Liang Sain 和 Kun Guo,在线零售行业的数据挖掘:基于 RFM 模型的数据挖掘客户细分案例研究,《数据库营销与客户战略管理杂志》,第 19 卷,第 3 期,第 197–208 页,2012(在线出版时间:2012 年 8 月 27 日。doi: 10.1057/dbm.2012.17)。
演示
您还可以在我的 YouTube 频道观看 RFM 建模的演示
请通过我的推荐链接加入 Medium。
[## 通过我的推荐链接加入 Medium - Pranay Dave
阅读 Pranay Dave 的每个故事(以及 Medium 上其他成千上万的作者的故事)。您的会员费直接支持…
pranay-dave9.medium.com](https://pranay-dave9.medium.com/membership?source=post_page-----640b527a677b--------------------------------)
请订阅以便在我发布新故事时保持信息更新
[## 每当 Pranay Dave 发布新内容时,您将收到一封电子邮件。
每当 Pranay Dave 发布新内容时,您将收到一封电子邮件。通过注册,如果您还没有 Medium 账户,您将创建一个…
pranay-dave9.medium.com](https://pranay-dave9.medium.com/subscribe?source=post_page-----640b527a677b--------------------------------)
额外资源
网站
您可以访问我的网站,这是一个无代码平台,用于学习数据科学。https://experiencedatascience.com
YouTube 频道
这是我的 YouTube 频道的链接
www.youtube.com/c/DataScienceDemonstrated
从数据仓库和数据湖到数据网格:企业数据架构指南
了解大型公司中的数据运作方式
·发布于 Towards Data Science ·19 min 阅读·2023 年 5 月 12 日
--
图片: Headway (Unsplash)
数据科学课程与现实世界中处理数据的现实之间存在脱节。
当我在半年前进入澳大利亚四大银行之一的第一个分析岗位时,我面临着一个复杂的数据环境,其特征为…
-
查找、访问和使用数据的挑战;
-
竞争的业务优先级将人们拉向不同的方向;
-
难以维护和升级的遗留系统;
-
对数据驱动洞察力有抵触的传统文化;
-
孤立的团队彼此之间没有沟通。
更新:我现在在 YouTube 上发布分析内容。
一段时间,我踏实地工作,接受了这样一个想法:也许这就是企业数据领域的现实。我相信,虽然我们的技术堆栈以非常快的速度发展,但用户体验最终会赶上…
我接受了数据科学培训,但实际上进行数据科学并非易事。在线课程并未为此做好准备。
但问题在这里。
经过一些挖掘,我意识到我的组织并不是唯一面临这些数据挑战的——这些挑战在整个行业中都是普遍存在的。
我们正处于技术创新的熔炉中,事物以惊人的速度发展。计算能力正在快速增长,机器学习应用已经无处不在,前沿的生成式 AI和AI 艺术正在颠覆各个行业,且没有停止的迹象,消费者的期望也在不断变化。
所有参与分析行业的人都在努力找到自己的立足点。我们都在一起跌跌撞撞地前行。快速失败,迅速前进。
这就是我撰写这篇文章的原因。
我想分享我的见解,并帮助像毕业生、新业务分析师和自学的数据科学家这样的专业人士快速了解企业级数据格局,并设定期望。
1. 数据是数字化的命脉
首先,让我们对数据在今天竞争激烈、节奏迅速的商业环境中扮演的关键角色达成一致。
各行各业的公司都在朝着数据驱动的决策方式迈进。
与此同时,消费者越来越期望超个性化的数字产品和服务,这些产品和服务利用像 AI 和机器学习这样的强大分析工具,这些工具在公司能掌握的所有优质数据上进行训练。
AI 和机器学习的世界如何与企业分析交汇。图片由作者提供
它使你能够按需观看个性化的电视节目(娱乐),订购食物并在一小时内送到(食品杂货与购物),以及在几分钟内获得预批准的抵押贷款(住房)。
这意味着一个前瞻性的数据堆栈对于生存和发展至关重要,因为数据是数字化的命脉。
正如英国数学家克莱夫·汉比在2006 年所说:
“数据是新的石油。”
IT 部门和数据平台不再是地下室里的“宅男”——它们现在是企业战略的核心部分。
数据现在是一个一流公民。
因为数据驱动一切。
所以不再废话,让我们深入探讨大型公司如何组织、处理和存储数据。
从我们的直升机俯瞰,你会看到景观被划分为操作数据和分析数据。
企业数据景观的 30,000 英尺视图。来源:Z. Dehghani 于MartinFowler.com,作者进行了修订
阅读我的解释 101,了解企业数据战略。
2. 操作(和事务)数据
操作数据通常以个别记录的形式出现,这些记录代表特定事件,例如销售、购买或客户互动,并且是企业依赖于其日常运营的信息。
操作数据存储在数据库中,并由微服务访问,微服务是帮助管理数据的小软件程序。数据不断更新,代表了业务的当前状态。
事务数据是一种重要的操作数据类型。银行中的事务示例包括:
-
银行账户之间的资金流动;
-
商品和服务的付款;
-
客户与我们某个渠道的互动,例如:分支机构或在线。
从应用程序中‘热出’的事务数据称为源数据,或记录系统(SOR)。源数据没有经过转换,它是……
-
数据科学家所偏好的数据格式;
-
进入数据湖的数据格式;
-
任何数据溯源的开始。
更多这些想法稍后会讲到。
称为在线事务处理(OLTP)系统的事务数据处理系统,必须快速处理许多事务。它们依赖于可以快速存储和检索数据的数据库,并通过实施称为ACID 语义的规则确保数据的准确性:
-
原子性 — 每个事务被视为一个单独的单位。我们不想把两个电汇或购买混在一起!
-
一致性 — 事务要么全部成功,要么全部失败。我购买的烤猪肉要么成功,要么失败!
-
隔离性 — 多个事务可以同时发生而不会互相干扰。这是可扩展性的核心原则。
-
持久性 — 即使系统关闭,数据变更也会被保存。丢失操作数据会使你的业务陷入困境。
OLTP 系统用于重要的业务应用程序,这些应用程序需要准确、快速且规模化地工作。
在银行业,OLTP 系统处理存款、取款、转账和余额查询,包括在线银行应用程序、信用卡和借记卡授权系统、支票处理器和在银行间转账的电汇系统。
正如你所见,OLTP 系统通常是企业与其客户之间的基本接口。
3. 分析数据
分析数据是公司运营或事务数据的时间(基于时间)和汇总(合并)视图。这提供了组织事实的汇总视图,旨在:
-
获得对过去业务表现的洞察(描述性和诊断分析);
-
为未来做出数据驱动的决策(预测和处方分析)。
从描述性分析到预测建模。作者提供的图片。
分析数据经常用于创建仪表板和报告(通常由数据分析师构建)和训练机器学习模型(数据科学家)。
查看我的逐步指南,了解如何训练回归和分类模型,使用 Python —— 当代数据科学的基本技能。
企业正朝着越来越强大的商业智能(BI)工具和无代码机器学习平台迈进,以努力实现数据和分析能力的民主化。
这个观点是,许多公司今天只拥有少量或孤岛的高级分析技能。游戏的改变者在于赋能全组织的 10,000 名非技术同事,提供合适的技能和工具,提升整体生产力,这超越了对一个 20 人数据科学团队进行微调的边际收益。(对不起,我的数据科学朋友们!)
所以关注数据民主化 —— 这现在是一个重要话题。
总体而言,分析处理不同于事务处理,因为前者专注于分析数据,而后者则专注于记录特定事件。
分析处理系统通常利用只读系统,这些系统存储大量的历史数据或业务指标。分析可以在某一时间点的数据快照上进行。
现在,让我们连接运营数据和分析数据之间的点。
操作数据通过数据管道转换为分析数据,这些管道通常由数据工程师构建。
这些“管道”通常是ETL 管道——涉及提取操作系统中的数据,转换以满足业务需求,并加载到数据仓库或数据湖中,以便分析。
ETL 管道连接操作数据存储和分析数据存储。图片由作者提供
阅读我的解释者 101 关于数据民主化。
4. 数据仓库与数据湖
整个分析数据层——企业存储其分析数据的地方——已分化为两种主要架构和技术栈:
-
数据仓库;
-
数据湖。
不同的用户可能在企业架构的不同阶段执行数据工作。
-
数据分析师通常在数据仓库中查询表格和聚合数据,以生成有效的仪表板、报告和可视化,这些业务用户和决策者会使用。
-
数据科学家通常在数据湖中工作,以探索在影子生产环境中的数据。这意味着在开发(即非生产)环境中对数据处理和建模进行原型设计,使用由数据工程师精心准备的实时(即生产)数据。一旦业务确认模型的价值,ML 工程师将其投入生产,以便模型可以在 24/7 运营团队(MLOps)的监督下为内部和外部客户提供服务。
数据仓库与数据湖。来源:Z. Dehghani 于 MartinFowler.com 由作者修订
对于那些新接触企业 IT 的人来说,你需要掌握两种主要的环境:
-
非生产环境,你可以在其中构建和尝试各种东西。变化代价低,破坏不会影响你的业务。也称为开发者环境。项目由组织的资本支出(CapEx)资助。
-
生产环境是你将最终完成并签署的应用程序、数据、管道和系统部署给真实客户的地方。你的工作现在是在线的。确保它很好,因为改变是昂贵的。生产环境——如口语中所称——是高度安全的封闭环境,由一个运维或运行团队负责,该团队的资金来自于组织的运营支出(OpEx)。我在这里写过有关资本支出与运营支出的更多内容。
简而言之,在非生产环境中构建东西,然后将其部署到生产环境中。明白了!
好的,让我们现在深入了解这两种数据架构的一些细节。
4.1 数据仓库
数据仓库是一种成熟的方式来存储结构化数据,采用关系模式,并且优化了读取操作——主要是 SQL 查询,用于支持 BI、报告和可视化。
数据仓库的一些特点:
-
历史分析:数据仓库几十年来一直是描述性分析的主力,提供了快速查询和连接大量历史数据的能力。
-
写时模式:数据仓库传统上采用写时模式的方法,其中表的结构或模式在前期就已定义。
一个常见的星型模式。图片由作者提供
-
数据建模:虽然数据分析师和数据科学家可以直接在分析数据存储中处理数据,但通常会创建数据模型来预先汇总数据,以便更容易生成报告、仪表板和交互式可视化。一个常见的数据模型——称为星型模式——基于包含你想要分析的数值的事实表(例如,某些与销售相关的金额),这些表与代表你想要测量的实体(例如客户或产品)的维度表相关——因此被称为关系型数据库。
-
快速查询:数据仓库中的数据可能会被汇总并加载到一个在线分析处理(OLAP)模型中,也称为立方体。来自事实表的数值(度量)会在一个或多个维度上预先汇总——例如,通过维度客户、产品和时间汇总的总收入(来自事实销售表)。从视觉上看,这就像是一个三维立方体中的三个维度的交集。从收益方面来看,OLAP/立方体模型捕捉了支持“钻取/上钻”分析的关系,查询速度很快,因为数据是预先汇总的。
“立方体”。度量(例如,销售)按时间、客户和产品维度进行汇总。图片由作者提供
- 文件类型:结构化数据文件包括可读格式如CSV和XLSX(Excel),以及优化格式如Avro、ORC和Parquet。关系数据库也可以存储半结构化数据,如JSON文件。
阅读我的Explainer 101关于数据仓库和数据建模。
4.2 数据湖
数据湖是存储大量基于文件的数据以支持数据科学和大规模分析数据处理场景的行业事实标准方法。
-
分布式计算与存储:数据湖使用分布式计算和分布式存储来处理和存储大量可能的非结构化数据。这意味着数据被存储和处理在可能数千台机器上,称为大数据集群。这种技术在 2010 年代兴起,由Apache Hadoop推动,这是一组开源大数据软件,使组织能够将大量数据分布到多台机器上(HDFS分布式存储)并对存储在这些机器上的表进行类似 SQL 的查询(Hive和Spark分布式计算)。Cloudera和Hortonworks等公司后来将 Apache 软件商业化,推出了简化全球组织入门和维护的软件包。
-
按需模式:数据湖使用按需模式,即只有在读取数据时才创建模式。这意味着数据可以批量存储在数据湖中,而不需要立即定义模式,从而允许在后续的特定用例中创建模式——这正是数据科学家进行建模所需的灵活性。
-
文件类型:数据湖是非结构化数据的家园——这包括像txt和doc这样的文本文件,像MP3和WAV这样的音频文件,像JPEG和PNG这样的图像文件,像MP4这样的视频文件,甚至包括整个 PDF 文件、社交媒体帖子、电子邮件、网页和传感器数据。数据湖(以及 NoSQL 数据库)也允许你按原样存储半结构化数据,如JSON和XML文件。
-
云计算:数据湖越来越多地托管在像Amazon Web Services、Microsoft Azure和Google Cloud这样的公共云提供商上。这种弹性和可扩展的基础设施使组织能够自动快速地调整计算和存储资源的变化需求,同时保持性能,并仅为实际使用的部分付费。有三种常见的云计算类型,它们在云提供商和客户端之间有不同的共享责任划分。最灵活的基础设施即服务(IaaS)允许你本质上租用数据中心中的空白空间。云提供商维护物理基础设施和互联网访问。相比之下,软件即服务(SaaS)模型中,客户端租用一个通过互联网运行的完全开发的软件解决方案(比如Microsoft Office)。对于企业数据,最受欢迎的云计算模型是中间地带的平台即服务(PaaS),其中提供商选择操作系统,客户端可以在其上构建数据架构和企业应用程序。
云计算类型与共享责任模型。作者提供的图片
- 实时分析与流处理:像Apache Kafka这样的技术使数据能够近乎实时地处理,作为持续的数据流,使公司能够解锁即时的洞察和趋势,或者对事件发生时采取立即响应的行动。例如,将及时的产品和服务推荐无缝地传递到客户的手机上,准确地在他们最可能受益的时间和地点——就像我的银行在我到达机场时为我提供旅行保险一样。或者考虑在客户即将向骗子转账时,能够实时主动地警告他们,从而保护他们的财务安全。这种大数据、快速低延迟计算和机器学习的全能融合是全球各行业超个性化服务的关键推动力。
阅读我的Explainer 101文章,了解云计算和企业生成式 AI。还可以查看我关于Azure Synapse & Fabric的文章——微软试图将数据仓库和大数据整合到一个平台下。
Ps…喜欢这个故事吗?当我发布类似的文章时,获取一份电子邮件。
数据网格与数据产品
架构师 Zhamek Dehghani 概述 了企业数据领域在三个代际中的演变——挑战、进展和失败:
第一代:专有的企业数据仓库和商业智能 平台;这些解决方案价格昂贵,导致公司积累了同样大量的技术债务 [以数千个不可维护的 ETL 作业的形式],以及只有少数专业人员理解的表格和报告,从而导致对业务的积极影响未得到充分发挥。
第二代:大数据生态系统,以数据湖作为灵丹妙药;复杂的大数据生态系统和由中央团队操作的长期批处理作业,创造了数据湖怪兽,这些怪兽最多只是支持了局部的研发分析;承诺过多而实现不足。
第三代(当前代)数据平台:与前一代类似,具有现代化的流式处理特点,支持实时数据可用性,架构统一批处理和流处理以进行数据转换,以及全面采用云管理服务来处理存储、数据管道执行引擎和机器学习平台。
当前的数据湖架构可以总结为:
-
集中式。所有分析数据都存储在一个地方,由一个没有数据领域知识的中央数据工程团队管理,这使得难以发挥数据的全部潜力或修复源头的数据质量问题。与去中心化架构相对,后者将数据摄取分散到整个业务团队。
-
与领域无关。一种力求服务所有人而不特别迎合任何人的架构。一个全能的平台。与领域驱动架构相对,后者中数据由不同的业务领域拥有。
-
单体。数据平台构建为一个难以更改和升级的大块。与模块化架构相对,后者允许调整和修改单个部分或微服务。
一个中央数据团队管理着一个单体的与领域无关的数据湖(还是数据怪兽?)。来源:数据网格架构(经许可)
问题很明显,某些解决方案似乎也已经显现。
进入 数据网格。
数据网格是 下一代 数据架构,它从单一的集中式数据团队转向去中心化的设计,其中数据由整个组织中最了解它的团队拥有和管理,这被称为领域驱动的拥有权。
重要的是,每个业务单元或领域都致力于注入产品思维,以创建高质量和可重用的数据产品——一个被数据的生产者视为产品的自包含和可访问的数据集——然后可以发布和分享给网格中其他领域和业务单元的消费者——这些被称为网格中的节点。
数据网格:各个业务单元共享精心构建的数据,符合‘产品标准’。来源:数据网格架构(已获许可)
数据网格使团队能够在更大的自主权和灵活性下独立工作,同时确保数据的一致性、可靠性和良好的治理。
这是我工作中的一个例子。
目前,我们客户的交易、产品、收入和负债数据都存放在我们集中式的数据湖中。(也包括我们的数据仓库。)
未来,随着我们在整个银行内联合能力和所有权,信用风险领域的自有数据工程师可以独立创建和管理他们的数据管道,而不依赖于一个远离业务并缺乏信用专业知识的集中式数据接收团队。
这个信用团队将自豪地构建和优化高质量、战略性和可重用的数据产品,这些产品可以分享给网格中的不同节点(业务领域),为抵押贷款团队提供可靠的信用信息,以便更好地决策批准住房贷款。
同样,这些数据产品也可以被消费者信用领域利用,开发机器学习模型,更好地了解我们的信用卡客户的行为,从而提供更好的服务并识别风险客户。
这些是利用数据产品在网格中发挥战略价值的例子。
网格的四个原则:领域所有权、数据即产品、自助平台、联合治理。来源:数据网格架构(已获许可)
数据网格培育了一种数据拥有和协作的文化,在这种文化中,数据被视为一级公民,并且进一步产品化,在团队和部门之间无缝共享,而不是在孤立团队为特定临时任务打造的经常重复的 ETL 管道中滞留。
数据网格促使组织从成本高昂且效率低下的基于项目的思维方式转向可扩展和前瞻性的基于产品的思维方式。
阅读我的Explainer 101文章,了解数据产品和数据网格。
6. 数据治理
数据治理就像一场大规模的谁是老板游戏,但针对数据。就像这个节目一样,有很多复杂的关系需要处理。
这涉及到确定谁负责哪些数据,谁可以访问数据,谁需要保护数据,以及有什么控制和监控措施来确保事情不会出错。
在我的工作场所,有 40,000 名员工,众多流程和竞争的优先事项,这使得维持秩序和确保每个人步调一致似乎是一项真正的挑战。
对于数据分析师、数据科学家和开发人员来说,数据治理可能像那个总是想知道你在干什么的烦人朋友。但对于组织,特别是那些受监管良好的组织,它们是绝对必要的。否则,这就像没有马戏团总指挥的马戏团——混乱、难以管理,并且非常危险。
数据治理组件。作者提供的图片
数据治理的一些核心考虑因素是:
数据隐私。这就像试图将你尴尬的童年照片藏起来不让世界看到。但对于企业来说,这比糟糕的发型严重得多。假设一家银行意外泄露了所有客户的财务信息。这将花费他们大量的现金,更重要的是,失去信任。
数据安全。你需要确保客户的数据不受外部威胁(如黑客)和内部威胁(如不良员工)的影响。这意味着要有强大的认证系统、容错的防火墙、坚不可摧的加密技术和全天候的网络安全监控。没有人愿意他们的数据被拍卖到黑暗网络上,落入罪犯之手。
数据质量。想象制作三明治——使用腐烂的食材,你会得到一顿糟糕的餐点。如果你的数据质量很差,你会得到没人愿意品尝的不可靠洞察。如果你处于受监管的行业,你最好确保你的三明治用的是新鲜的食材,否则你的数据可能无法满足合规要求。
维护有关数据在企业中流动的可靠信息——即数据血缘——对于确保数据质量和在出现问题时进行故障排除至关重要。
数据隐私、数据安全和/或数据质量薄弱意味着更多的数据风险。
这就是数据所有权的作用。谁来决定数据的使用和管理?谁承担了风险,以防出现问题?
在实践中,这有点像热土豆游戏,没人真正想长时间握住那个土豆。但总得有人负责,这样我们才能避免数据问题,并保持我们的土豆热乎、新鲜和安全。
向 mesh 的转变旨在:
-
提升全方位的数据质量(通过可重用的数据产品);
-
优化数据所有权(让适当的领域拥有他们的数据)。
-
简化数据血缘(告别分散的 ETL,转向集中化的数据湖)。
请参阅我的解释员 101文章,了解数据治理。
7. 最后的话
企业级数据领域常常令人困惑,技术债务的积累源于实验周期后的过度修正,类似于股市的波动。
尽管大型公司的故事各不相同,但它们有一些共同点。其中之一是有机扩展至一个庞大而令人畏惧的企业数据仓库,随后是热切接受一个集中式数据湖,旨在节省成本、集中专业知识并放大数据价值。
这种方法带来了全新的问题。因此我们再次回到了起点——这一次大幅度地向去中心化数据堆栈转变,将数据管理下放到最了解自己数据的团队。
呼!就像一群企鹅在不断变化的冰面上缓慢移动。
以个人背景为例,我所在的银行经历了本文中描述的所有数据架构时代。
我们花了几十年时间在数据仓库上。然后,我们开始了现在已经持续了 7 年的旅程,旨在建立一个战略数据湖,成为我们数据基础设施的基石。
长话短说,我们的数据仓库和数据湖现在依然存在,且以一种有些尴尬的方式共存。(这仍在持续改进中……)
我们已经开始了自己的旅程,将数据湖去中心化向网状架构。我们正在通过利用可重用数据产品的力量,破解我们数据领域的意大利面般复杂性。
目前,大公司专注于解决在其数据领域积累的几十年的技术债务。图片由作者提供
我很自豪地说,在澳大利亚的四大银行中,我们显然领先。这非常令人高兴,因为大型蓝筹公司通常不在技术创新的前沿。
像许多公司一样,我们面临着巨大的挑战,因为这些技术债务是数百个项目的副产品,由多年来进进出出的数千位同事推动。
我的在线数据科学课程——由公司慷慨赞助——教会了我如何处理数据和训练逻辑回归模型及梯度提升树,但对在大型组织中处理数据的现实准备不足。
在我第一天上班时,我以为会被直接提供一些美味的数据,然后立即开始模型训练。
希望我已经走得很远了,远离达宁-克鲁格曲线?来源:维基百科
就像福雷斯特·甘普发现的那样,生活并不像想象中那么简单。
通过尝试和失败——我亲身体验到,作为数据科学家,有许多技能决定了你的影响力,远超课程所能提供的——从业务参与到数据讲述,再到政治博弈和不断演变的企业数据环境中的所有复杂细节。
通过撰写这篇文章,我希望能让你避免一些我自己曾经遇到的绊脚石。
告诉我你是否在自己的旅程中也有类似的经历!
在 Twitter 和 YouTube 上找到我,在这里、在这里 和 在这里。
我的热门 AI、ML 和数据科学文章
-
AI 和机器学习:快速入门——在这里
-
机器学习与机制建模——在这里
-
数据科学:现代数据科学家所需的新技能——在这里
-
生成性 AI:大公司如何争相采用——在这里
-
ChatGPT & GPT-4: OpenAI 如何赢得 NLU 战争——在这里
-
生成性 AI 艺术:DALL-E、Midjourney 和 Stable Diffusion 解析——在这里
-
超越 ChatGPT:寻找真正智能的机器——在这里
-
现代企业数据战略解析——在这里
-
从数据仓库与数据湖到数据网格——在这里
-
从数据湖到数据网格:最新架构指南——在这里
-
Azure Synapse Analytics 实战:7 个用例解析——在这里
-
云计算 101:为您的业务利用云——在这里
-
数据仓库与数据建模 — 快速入门课程 — 这里
-
数据产品:为分析建立坚实的基础 — 这里
-
数据民主化:5 个‘数据为所有人’策略 — 这里
-
数据治理:分析师的 5 大常见痛点 — 这里
-
数据讲故事的力量 — 销售故事,而非数据 — 这里
-
数据分析简介:谷歌方法 — 这里
-
Power BI — 从数据建模到惊艳报告 — 这里
-
回归分析:使用 Python 预测房价 — 这里
-
分类:使用 Python 预测员工流失 — 这里
-
Python Jupyter 笔记本与 Dataiku DSS — 这里
-
流行的机器学习性能指标解释 — 这里
-
在 AWS 上构建 GenAI — 我的第一次体验 — 这里
-
数学建模与 COVID-19 的机器学习 — 这里
-
工作的未来:在人工智能时代你的职业安全吗 — 这里
从决策树到变换器:比较马其顿餐厅评论的情感分析模型
分析马其顿餐厅评论的机器学习技术
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 3 月 3 日
--
作者提供的图形
- 尽管自然语言处理的机器学习模型传统上集中于英语和西班牙语等流行语言,但较少使用的语言的发展则相对较少。然而,由于 COVID-19 大流行导致电子商务的兴起,即使是像马其顿语这样较少使用的语言也通过在线评论产生了大量数据。这为开发和训练马其顿语餐馆评论的情感分析机器学习模型提供了机会,这可以帮助企业更好地理解客户情感并改善服务。在这项研究中,我们解决了这一问题中出现的挑战,并探讨和比较了用于分析马其顿餐馆评论情感的各种情感分析模型,从经典的随机森林到现代的深度学习技术和变压器。
内容
-
挑战与数据预处理
-
创建向量嵌入
-
LASER 嵌入
-
多语言通用文本编码器
-
OpenAI Ada v2
-
-
机器学习模型
-
随机森林
-
XGBoost
-
支持向量机
-
深度学习
-
变压器
-
-
结果与讨论
-
未来工作
-
结论
数据预处理
-
语言是独特的人类交流工具,计算机在没有适当处理技术的情况下无法解释它。为了让机器分析和理解语言,我们需要以计算机可处理的方式表示复杂的语义和词汇信息。实现这一目标的一种流行方法是使用向量表示。近年来,除了特定语言的表示模型外,多语言模型也出现了。这些模型可以捕捉大量语言的文本语义上下文。
-
然而,对于使用西里尔字母的语言,额外的挑战是由于互联网用户经常使用拉丁字母表达自己,导致数据中混合了拉丁和西里尔文本。为了解决这一挑战,我使用了来自本地餐馆的大约 500 条评论的数据集,其中包含拉丁和西里尔字母。该数据集还包括一小部分英文评论,有助于评估在混合数据上的表现。此外,在线文本可能包含如表情符号等需要去除的符号。因此,数据预处理是进行任何文本嵌入之前的关键步骤。
import pandas as pd
import numpy as np
# load the dataset into a dataframe
df = pd.read_csv('/content/data.tsv', sep='\t')
# see the distribution of the sentiment classes
df['sentiment'].value_counts()
# -------
# 0 337
# 1 322
# Name: sentiment, dtype: int64
- 数据集包含正类和负类,分布几乎相等。为了去除表情符号,我使用了 Python 库
emoji
,它可以轻松去除表情符号和其他符号。
!pip install emoji
import emoji
clt = []
for comm in df['comment'].to_numpy():
clt.append(emoji.replace_emoji(comm, replace=""))
df['comment'] = clt
df.head()
- 对于西里尔和拉丁文本的问题,我将所有文本转换成其中一种,以便在两者上测试机器学习模型,以比较其性能。我使用了“cyrtranslit”库来完成这一任务。它支持大多数西里尔字母,如马其顿语、保加利亚语、乌克兰语等。
import cyrtranslit
latin = []
cyrillic = []
for comm in df['comment'].to_numpy():
latin.append(cyrtranslit.to_latin(comm, "mk"))
cyrillic.append(cyrtranslit.to_cyrillic(comm, "mk"))
df['comment_cyrillic'] = cyrillic
df['comment_latin'] = latin
df.head()
图 1. 转换后的输出
对于我使用的嵌入模型,通常不需要移除标点符号、停用词及其他文本清理。这些模型设计用来处理自然语言文本,包括标点符号,并且当文本保持完整时,通常能够更准确地捕捉句子的含义。这样文本的预处理就完成了。
向量嵌入
目前,没有大规模的马其顿语表示模型可用。然而,我们可以使用在马其顿语文本上训练的多语言模型。虽然有几个这样的模型可用,但对于这项任务,我发现 LASER 和 Multilingual Universal Sentence Encoder 是最合适的选择。
LASER
LASER(语言无关句子表示)是一种生成高质量多语言句子嵌入的语言无关方法。LASER 模型基于一个两阶段过程,其中第一阶段是预处理文本,包括分词、小写转换和应用 sentencepiece。这部分是特定语言的。第二阶段涉及将预处理后的输入文本映射到固定长度的嵌入,使用的是多层双向 LSTM。
LASER 已被证明在一系列基准数据集上优于其他流行的句子嵌入方法,如 fastText 和 InferSent。此外,LASER 模型是开源的且免费提供,方便所有人使用。
使用 LASER 创建嵌入是一个简单的过程:
!pip install laserembeddings
!python -m laserembeddings download-models
from laserembeddings import Laser
# create the embeddings
laser = Laser()
embeddings_c = laser.embed_sentences(df['comment_cyrillic'].to_numpy(),lang='mk')
embeddings_l = laser.embed_sentences(df['comment_latin'].to_numpy(),lang='mk')
# save the embeddings
np.save('/content/laser_multi_c.npy', embeddings_c)
np.save('/content/laser_multi_l.npy', embeddings_l)
多语言通用句子编码器
多语言通用句子编码器(MUSE)是一个预训练模型,用于生成句子嵌入,由 Facebook 开发。MUSE 旨在将多种语言的句子编码到一个共同的空间中。
该模型基于一个深度神经网络,使用编码器-解码器架构来学习句子与其对应的高维空间嵌入向量之间的映射。MUSE 在一个大规模的多语言语料库上进行训练,该语料库包括维基百科、新闻文章和网页文本。
!pip install tensorflow_text
import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
import tensorflow_text
# load the MUSE module
module_url = "https://tfhub.dev/google/universal-sentence-encoder-multilingual-large/3"
embed = hub.load(module_url)
sentences = df['comment_cyrillic'].to_numpy()
muse_c = embed(sentences)
muse_c = np.array(muse_c)
sentences = df['comment_latin'].to_numpy()
muse_l = embed(sentences)
muse_l = np.array(muse_l)
np.save('/content/muse_c.npy', muse_c)
np.save('/content/muse_l.npy', muse_l)
OpenAI Ada v2
在 2022 年底,OpenAI 宣布了他们全新的最先进嵌入模型 text-embedding-ada-002。由于该模型建立在 GPT-3 之上,它具有多语言处理能力。为了比较斯拉夫字母和拉丁字母评论之间的结果,我在这两个数据集上运行了该模型。
!pip install openai
import openai
openai.api_key = 'YOUR_KEY_HERE'
embeds_c = openai.Embedding.create(input = df['comment_cyrillic'].to_numpy().tolist(), model='text-embedding-ada-002')['data']
embeds_l = openai.Embedding.create(input = df['comment_latin'].to_numpy().tolist(), model='text-embedding-ada-002')['data']
full_arr_c = []
for e in embeds_c:
full_arr_c.append(e['embedding'])
full_arr_c = np.array(full_arr_c)
full_arr_l = []
for e in embeds_l:
full_arr_l.append(e['embedding'])
full_arr_l = np.array(full_arr_l)
np.save('/content/openai_ada_c.npy', full_arr_c)
np.save('/content/openai_ada_l.npy', full_arr_l)
机器学习模型
本节探讨了用于预测马其顿餐馆评论情感的各种机器学习模型。从传统机器学习模型到深度学习技术,我们将深入研究每种模型的优缺点,并比较它们在数据集上的表现。
在运行任何模型之前,应将数据按每种嵌入类型分为训练集和测试集。这可以通过sklearn
库轻松完成。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(embeddings_c, df['sentiment'], test_size=0.2, random_state=42)
随机森林
图 2. 随机森林分类的简化表示。构建了 100 棵决策树,结果通过对每棵决策树的结果进行多数投票来计算。(作者绘制)
随机森林是一种广泛使用的机器学习算法,它使用决策树的集成来分类数据点。该算法通过在完整数据集的子集和特征的随机子集上训练每棵决策树来工作。在推断过程中,每棵决策树生成情感的预测,最终输出通过对所有树的多数投票来获得。这种方法有助于防止过拟合,并能导致更稳健和准确的预测。
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
rfc = RandomForestClassifier(n_estimators=100)
rfc.fit(X_train, y_train)
print(classification_report(y_test,rfc.predict(X_test)))
print(confusion_matrix(y_test,rfc.predict(X_test)))
XGBoost
图 3. 提升基算法的顺序过程。每棵后续决策树在前一棵树的残差(错误)上进行训练。(作者绘制)
XGBoost(极端梯度提升)是一种强大的集成方法,主要用于表格数据。与随机森林一样,XGBoost 也使用决策树来分类数据点,但采用不同的方法。XGBoost 不是一次训练所有树,而是以顺序方式训练每棵树,从前一棵树的错误中学习。这一过程称为提升,即将弱模型组合成一个更强的模型。尽管 XGBoost 主要在表格数据上产生出色的结果,但用向量嵌入测试它也很有趣。
from xgboost import XGBClassifier
from sklearn.metrics import classification_report, confusion_matrix
rfc = XGBClassifier(max_depth=15)
rfc.fit(X_train, y_train)
print(classification_report(y_test,rfc.predict(X_test)))
print(confusion_matrix(y_test,rfc.predict(X_test)))
支持向量机
图 4. 支持向量分类的简化表示。在具有 1024 个输入特征的情感分析中,超平面将是 1023 维的。(作者绘制)
支持向量机(SVM)是一种流行且强大的机器学习算法,用于分类和回归任务。它通过寻找将数据分成不同类别的最佳超平面,同时最大化类别之间的间隔来工作。SVM 特别适用于高维数据,并可以使用核函数处理非线性边界。
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix
rfc = SVC()
rfc.fit(X_train, y_train)
print(classification_report(y_test,rfc.predict(X_test)))
print(confusion_matrix(y_test,rfc.predict(X_test)))
深度学习
图 5. 该问题中使用的神经网络的简化表示。(作者绘制)
深度学习是一种先进的机器学习方法,利用由多层神经元组成的人工神经网络。深度学习网络在文本和图像数据上表现出色。使用库 Keras 实现这些网络是一个简单的过程。
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
model = keras.Sequential()
model.add(keras.layers.Dense(256, activation='relu', input_shape=(1024,)))
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.Dense(128, activation='relu'))
model.add(keras.layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
history = model.fit(X_train, y_train, epochs=11, validation_data=(X_test, y_test))
test_loss, test_acc = model.evaluate(X_test, y_test)
print('Test accuracy:', test_acc)
y_pred = model.predict(X_test)
print(classification_report(y_test,y_pred.round()))
print(confusion_matrix(y_test,y_pred.round()))
在这里,使用了具有两个隐藏层和一个整流线性单元(ReLU)激活函数的神经网络。输出层包含一个带有 sigmoid 激活函数的单一神经元,使得网络能够对正面或负面情感进行二分类预测。二元交叉熵损失函数与 sigmoid 激活函数配对,用于训练模型。此外,还使用了 Dropout 来帮助防止过拟合,并提高模型的泛化能力。我测试了各种不同的超参数,发现这种配置最适合这个问题。
使用以下函数我们可以可视化模型的训练过程。
import matplotlib.pyplot as plt
def plot_accuracy(history):
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Model Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(['Train', 'Validation'], loc='upper left')
plt.show()
图 6. 示例训练输出
变换器
图 7. BERT 大型语言模型的预训练和微调过程。(来源:原始 BERT 论文)
微调变换器是自然语言处理中的一种流行技术,涉及调整预训练的变换器模型以适应特定任务。变换器,如 BERT、GPT-2 和 RoBERTa,经过大量文本数据的预训练,能够学习语言中的复杂模式和关系。然而,为了在特定任务上表现良好,如情感分析或文本分类,这些模型需要在任务特定的数据上进行微调。
对于这些类型的模型,我们之前创建的向量表示是不需要的,因为它们直接处理从文本中提取的标记。对于马其顿的情感分析任务,我使用了 bert-base-multilingual-uncased
,这是 BERT 模型的多语言版本。
HuggingFace 使得微调变换器变得非常简单。首先需要将数据加载到变换器数据集中。然后对文本进行标记化,最后训练模型。
from sklearn.model_selection import train_test_split
from datasets import load_dataset
from transformers import TrainingArguments, Trainer
from sklearn.metrics import classification_report, confusion_matrix
# create csv of train and test sets to be loaded by the dataset
df.rename(columns={"sentiment": "label"}, inplace=True)
train, test = train_test_split(df, test_size=0.2)
pd.DataFrame(train).to_csv('train.csv',index=False)
pd.DataFrame(test).to_csv('test.csv',index=False)
# load the dataset
dataset = load_dataset("csv", data_files={"train": "train.csv", "test": "test.csv"})
# tokenize the text
tokenizer = AutoTokenizer.from_pretrained('bert-base-multilingual-uncased')
encoded_dataset = dataset.map(lambda t: tokenizer(t['comment_cyrillic'], truncation=True), batched=True,load_from_cache_file=False)
# load the pretrained model
model = AutoModelForSequenceClassification.from_pretrained('bert-base-multilingual-uncased',num_labels =2)
# fine-tune the model
arg = TrainingArguments(
"mbert-sentiment-mk",
learning_rate=5e-5,
num_train_epochs=5,
per_device_eval_batch_size=8,
per_device_train_batch_size=8,
seed=42,
push_to_hub=True
)
trainer = Trainer(
model=model,
args=arg,
tokenizer=tokenizer,
train_dataset=encoded_dataset['train'],
eval_dataset=encoded_dataset['test']
)
trainer.train()
# get predictions
predictions = trainer.predict(encoded_dataset["test"])
preds = np.argmax(predictions.predictions, axis=-1)
# evaluate
print(classification_report(predictions.label_ids,preds))
print(confusion_matrix(predictions.label_ids,preds))
我们已经成功地对 BERT 进行了情感分析的微调。
结果与讨论
图 8. 所有模型的结果
对马其顿餐馆评论的情感分析结果令人鼓舞,多个模型达到了高精度和 F1 分数。实验表明,深度学习模型和变换器在表现上优于传统的机器学习模型,如随机森林和支持向量机,尽管差距不大。使用新的 OpenAI 嵌入的变换器和深度神经网络成功突破了 0.9 的准确率障碍。
OpenAI 嵌入模型 textembedding-ada-002
大幅提升了即使是经典机器学习模型的结果,尤其是在支持向量机上。在这项研究中,使用该嵌入在深度学习模型上对西里尔文本取得了最佳结果。
总的来说,拉丁文本的表现比西里尔文本差。虽然我最初假设这些模型的表现会更好,考虑到拉丁语中类似词汇的普遍性以及嵌入模型是基于这种数据训练的,但研究结果并未支持这一假设。
未来工作
在未来的工作中,收集更多数据以进一步训练和测试模型将非常有价值,特别是涵盖更多样化的评论主题和来源。此外,尝试将更多特征(如元数据(例如,评论者的年龄、性别、位置)或时间信息(例如,评论时间))纳入模型中,可能会提高其准确性。最后,将分析扩展到其他不那么常见的语言,并比较这些模型与马其顿评论训练的模型的表现,将是很有趣的。
结论
总结来说,本文展示了多种机器学习模型和嵌入技术在马其顿餐馆评论情感分析中的有效性。探讨并比较了几种经典的机器学习模型,如随机森林和支持向量机,以及现代深度学习技术,包括神经网络和变压器。结果表明,经过微调的变压器模型和使用最新 OpenAI 嵌入的深度学习模型表现优于其他方法,验证准确率高达 90%。
感谢阅读!