用线性规划设计能源套利策略
如何利用 Python 中的真实数据和开源工具(包括 PuLP 和 pandas ),使用经典的数学优化技术来指导并网电池的运行并实现利润最大化。
电力线,照片由 Pexels 的 Pok Rie 拍摄
能源的价格每小时都在变化,这为时间套利提供了可能性:低价购买能源,储存起来,然后以更高的价格出售。要成功执行任何时间套利策略,需要对未来价格有一定的信心,才能期望获利。在能量套利的情况下,还必须考虑能量存储系统的约束。例如,电池具有有限的容量、有限的充电速率,并且不是 100%有效的,因为不是所有用于对电池充电的能量都可以在以后用于放电。
本文的目的是说明在已知未来价格的假设下,如何使用线性规划的数学优化技术,在给定一组操作约束的情况下,为并网电池设计操作策略。所有这些都将使用真实世界的能源价格数据和 Python 中的开源工具来完成。Jupyter 笔记本可在此处获得。
问题和方法
任务是为并网电池系统制定运营策略,在纽约独立系统运营商(NYISO)日前市场进行能源套利。什么是尼索?根据他们的网站,
ny iso 是纽约独立系统运营商,负责管理纽约电网及其竞争激烈的电力批发市场。
纽约国际标准化组织每天上午 11 点公布第二天的每小时能源价格(纽约 ISO 2019,参考资料见下页)。我们在这里设计的电池系统将利用这些信息安排接下来 24 小时的电池工作(当天中午到第二天上午 11 点)。我们将假设在 NYISO 发布每日价格公告后,接下来 36 小时的价格信息是可用的:当天剩余 12 小时的价格,以及第二天 24 小时的价格。因此,优化时间范围将是 36 小时,以利用所有可用的未来价格信息。
由于每天都会重复运行时间表,因此 36 小时策略的最后 12 小时将始终被忽略。这是因为新的价格信息将在第二天上午 11 点提供,我们可以利用这一点。然而,这些“额外的 12 小时”的数据并没有被浪费;我在最初的实验中确定,36 小时的时间范围比更短的时间范围更有利可图。这具有直观的意义,因为我们能够纳入套利策略的未来信息越多,就应该越有利可图。您可以使用下面的代码尝试不同的时间范围,尽管这里假设时间范围至少为 24 小时。该计划可以想象如下:
电池被称为价格接受者,这意味着它的活动不会影响能源的价格。为电池充电而支付的电力价格和放电收入是基于位置的边际价格(LBMP),它考虑了系统边际价格、阻塞部分和边际损耗部分(PJM 互联有限责任公司)。目标是在给定日前价格和电池系统参数的情况下,实现利润最大化。
在这种情况下,未来价格已知,电池系统是价格接受者,设计运营策略的问题可以通过线性规划来解决(Salles et al. 2017,Sioshansi et al. 2009,Wang and Zhang 2018)。简而言之,线性规划是一种众所周知的技术,用于最大化或最小化某些目标。在这种情况下,我们希望利润最大化。只要描述目标的数学函数,即所谓的目标函数,以及系统的约束,都可以描述为定义操作策略的决策变量的线性组合,就可以使用线性规划来优化系统。
纸浆中线性规划问题的建立
大致遵循 Sioshansi 等人(2009 年)的符号,这里我们将列出决策变量,并将约束添加到 PuLP 中的线性规划模型。从下一个单元格中的类定义开始,本节中的 markdown 代码片段应该放在一起定义一个类,该类描述了我们的电池系统(请参见笔记本进行确认)。这种系统模型将有助于模拟电池的运行,以每天一次的增量逐步通过时间。
在继续之前,让我们导入本练习所需的包。
*#Load packages* import pulp
import os
import numpy **as** np
import pandas **as** pd
import matplotlib **as** mpl
import matplotlib.pyplot **as** plt
import time
**%**matplotlib inline
每天,在上午 11 点到下午 12 点之间需要解决一个优化问题,这将提供足够的信息来指导电池在接下来的 36 个小时内的运行。为了指导电池的运行,我们需要确定时间范围内每个离散时间步长的能量流。能量流,也称为电能,可以流入或流出电池。因此,我们将在时间 t 创建两个决策变量, c_t 和 *d_t,*分别作为充电和放电功率流(kW),这将是一个小时的时间步长。对于所有时间步长,充电或放电速率是连续变量,并且被限制在电池的工作极限内:
其中 κ (kW)是最大充电和放电功率容量,这里我们假设它们相等。
我们需要在优化的时间范围内为每个时间步长指定流量变量。PuLP 提供了一个LpVariable
类的便捷方法dicts
,我们可以用它来一次为所有时间步骤创建充电和放电流。我们将开始定义一个类来保存管理电池运行的所有信息。输入time_horizon
是一个整数,指定优化范围内的小时数,这里假设至少为 24,其他输入如上所述:
**class** **Battery**():
**def** **__init__**(self,
time_horizon,
max_discharge_power_capacity,
max_charge_power_capacity):
*#Set up decision variables for optimization.
* *#These are the hourly charge and discharge flows for
* *#the optimization horizon, with their limitations.
* self**.**time_horizon **=** time_horizon
self**.**charge **=** \
pulp**.**LpVariable**.**dicts(
"charging_power",
('c_t_' **+** str(i) **for** i **in** range(0,time_horizon)),
lowBound**=**0, upBound**=**max_charge_power_capacity,
cat**=**'Continuous')
self**.**discharge **=** \
pulp**.**LpVariable**.**dicts(
"discharging_power",
('d_t_' **+** str(i) **for** i **in** range(0,time_horizon)),
lowBound**=**0, upBound**=**max_discharge_power_capacity,
cat**=**'Continuous')
设置好所有的决策变量后,就该定义纸浆将为我们解决的优化问题了。我们的目标是在优化时间范围内最大化利润 P ,可使用电荷流和能源价格定义如下:
其中 p_t 是时间 t 时的 LBMP ($/MWh)。就单位而言,因为 t 是一个小时时间步长,它有效地取消了 LBMP 单位的每小时部分。目标函数除以 1000 以说明单位(MW/kW)的差异,因此最终利润的目标将以美元为单位。我们将在运行模拟后进行这个单元修正,所以它不会反映在这里的代码中。
目标函数是通过将决策变量添加到纸浆中的模型对象来指定的,将每个决策变量乘以适当的成本或收入,该成本或收入将来自充入或释放该量的能量。决策变量与价格的相乘是使用LpAffineExpression
完成的。prices
由 LBMP 决定,我们将在运行模拟时选择相关的时间段。例如,对于 36 小时的时间范围,这将是当天中午到第二天晚上 11 点。
**def** **set_objective**(self, prices):
*#Create a model and objective function.
* *#This uses price data, which must have one price
* *#for each point in the time horizon.
* **try**:
**assert** len(prices) **==** self**.**time_horizon
**except**:
**print**('Error: need one price for each hour in time horizon')
*#Instantiate linear programming model to maximize the objective
* self**.**model **=** pulp**.**LpProblem("Energy arbitrage", pulp**.**LpMaximize)
*#Objective is profit
* *#This formula gives the daily profit from charging/discharging
* *#activities. Charging is a cost, discharging is a revenue
* self**.**model **+=** \
pulp**.**LpAffineExpression(
[(self**.**charge['c_t_' **+** str(i)],
**-**1*****prices[i]) **for** i **in** range(0,self**.**time_horizon)]) **+**\
pulp**.**LpAffineExpression(
[(self**.**discharge['d_t_' **+** str(i)],
prices[i]) **for** i **in** range(0,self**.**time_horizon)])
已经定义了模型和目标,现在我们需要添加电池的操作约束。电池容量有限,因此优化受电池存储限制:
其中 t_f =从当天下午 1 点到第二天晚上 11 点的每个小时, s_i 是 36 小时优化期开始时电池的能量状态(kWh)η是电池的往返效率。该约束要求电池的能量状态(初始状态和每小时功率流的总和)在零(假设电池具有完全深度的放电能力)和电池在优化范围内每小时的放电能量容量之间。在此约束条件下,功率流(kW)被理解为通过乘以一小时时间步长转换为能量单位(kWh)。
在 PuLP 中,约束可以像目标函数一样添加到模型中:使用加法语法。我们可以通过使用lpSum
将放电流量相加来表达这些约束,如果充电流量需要乘以效率,则再次使用LpAffineExpression
。
**def** **add_storage_constraints**(self,
efficiency,
min_capacity,
discharge_energy_capacity,
initial_level):
*#Storage level constraint 1
* *#This says the battery cannot have less than zero energy, at
* *#any hour in the horizon
* *#Note this is a place where round-trip efficiency is factored in.
* *#The energy available for discharge is the round-trip efficiency
* *#times the energy that was charged.
* **for** hour_of_sim **in** range(1,self**.**time_horizon**+**1):
self**.**model **+=** \
initial_level \
**+** pulp**.**LpAffineExpression(
[(self**.**charge['c_t_' **+** str(i)], efficiency)
**for** i **in** range(0,hour_of_sim)]) \
**-** pulp**.**lpSum(
self**.**discharge[index]
**for** index **in**('d_t_' **+** str(i)
**for** i **in** range(0,hour_of_sim)))\
**>=** min_capacity
*#Storage level constraint 2
* *#Similar to 1
* *#This says the battery cannot have more than the
* *#discharge energy capacity
* **for** hour_of_sim **in** range(1,self**.**time_horizon**+**1):
self**.**model **+=** \
initial_level \
**+** pulp**.**LpAffineExpression(
[(self**.**charge['c_t_' **+** str(i)], efficiency)
**for** i **in** range(0,hour_of_sim)]) \
**-** pulp**.**lpSum(
self**.**discharge[index]
**for** index **in** ('d_t_' **+** str(i)
**for** i **in** range(0,hour_of_sim)))\
**<=** discharge_energy_capacity
最大日放电吞吐量 τ (kWh)也受到限制,这限制了在给定的一天内可以流过电池的能量。我们对此进行了设置,以便时间范围的第一天受 24 小时约束,而超出该时间范围的任何部分都受部分约束。例如,在我们的 36 小时范围内,约束条件是:
**def** **add_throughput_constraints**(self,
max_daily_discharged_throughput):
*#Maximum discharge throughput constraint
* *#The sum of all discharge flow within a day cannot exceed this
* *#Include portion of the next day according to time horizon
* *#Assumes the time horizon is at least 24 hours
*
self**.**model **+=** \
pulp**.**lpSum(
self**.**discharge[index] **for** index **in** (
'd_t_' **+** str(i) **for** i **in** range(0,24))) \
**<=** max_daily_discharged_throughput
self**.**model **+=** \
pulp**.**lpSum(
self**.**discharge[index] **for** index **in** (
'd_t_' **+** str(i) **for** i **in** range(25,self**.**time_horizon))) \
**<=** max_daily_discharged_throughput \
*****float(self**.**time_horizon**-**24)**/**24
既然我们已经建立了具有目标函数和所有约束的模型,我们包括解决问题的方法,并报告结果,即在时间范围内每小时的最佳充电和放电流量。只要我们提出的问题在我们指出的约束条件下是可行的,线性规划就应该工作,因为它会找到一个最优解,使利润最大化。然而,如果没有,我们将返回一条消息指出这一点。
**def** **solve_model**(self):
*#Solve the optimization problem
* self**.**model**.**solve()
*#Show a warning if an optimal solution was not found
* **if** pulp**.**LpStatus[self**.**model**.**status] **!=** 'Optimal':
**print**('Warning: ' **+** pulp**.**LpStatus[self**.**model**.**status])
**def** **collect_output**(self):
*#Collect hourly charging and discharging rates within the
* *#time horizon
* hourly_charges **=**\
np**.**array(
[self**.**charge[index]**.**varValue **for**
index **in** ('c_t_' **+** str(i) **for** i **in** range(0,24))])
hourly_discharges **=**\
np**.**array(
[self**.**discharge[index]**.**varValue **for**
index **in** ('d_t_' **+** str(i) **for** i **in** range(0,24))])
**return** hourly_charges, hourly_discharges
这就完成了Battery
课。我们现在准备接收数据,并继续模拟电池工作。
导入价格数据
我们获得了一年的 LBMPs 值,因此我们可以模拟电池在这段时间内的运行情况。这些数据可作为几个区域的 LBMPs,以每小时为时间步长。这里我们加载 CSV 文件(每天一个)并将它们连接到一个DataFrame
。您可以从这篇博文附带的 git repo 中获得这些数据。数据于 2020 年 5 月 2 日从这里下载,作为 CSV 文件的压缩目录(定价数据、LBMP 日前市场(DAM)、区域 P-2A)。
*#Directory of data* data_dir **=** './data_2019_2020_from_web/'dir_list **=** os**.**listdir(data_dir)
dir_list**.**sort()
dir_list['.DS_Store',
'20190501damlbmp_zone_csv',
'20190601damlbmp_zone_csv',
'20190701damlbmp_zone_csv',
'20190801damlbmp_zone_csv',
'20190901damlbmp_zone_csv',
'20191001damlbmp_zone_csv',
'20191101damlbmp_zone_csv',
'20191201damlbmp_zone_csv',
'20200101damlbmp_zone_csv',
'20200201damlbmp_zone_csv',
'20200301damlbmp_zone_csv',
'20200401damlbmp_zone_csv']*#Remove invisible files (i.e. .DS_Store used by Mac OS)* **for** this_item **in** dir_list:
**if** this_item[0] **==** '.':
dir_list**.**remove(this_item)
遍历所有子目录,加载所有 CSV 文件。
tic **=** time**.**time()
*#count loaded files* file_counter **=** 0
*#For each subdirectory in the parent directory* **for** this_sub_dir **in** dir_list:
*#List the files
* this_sub_dir_list **=** os**.**listdir(data_dir **+** '/' **+** this_sub_dir)
*#Sort the list
* this_sub_dir_list**.**sort()
*#Delete invisible files (that start with '.')
* **for** this_item **in** this_sub_dir_list:
**if** this_item[0] **==** '.':
this_sub_dir_list**.**remove(this_item)
*#For each file in the subdirectory
* **for** this_file **in** this_sub_dir_list:
*#Load the contents into a DataFrame
* this_df **=** pd**.**read_csv(data_dir **+** '/' **+** this_sub_dir **+** '/' **+** this_file)
*#Concatenate with existing data if past first file
* **if** file_counter **==** 0:
all_data **=** this_df**.**copy()
**else**:
all_data **=** pd**.**concat([all_data, this_df])
file_counter **+=** 1
toc **=** time**.**time()
**print**(str(toc**-**tic) **+** ' seconds run time')2.1731250286102295 seconds run time
检查数据
all_data**.**info()<class 'pandas.core.frame.DataFrame'>
Int64Index: 131760 entries, 0 to 359
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Time Stamp 131760 non-null object
1 Name 131760 non-null object
2 PTID 131760 non-null int64
3 LBMP ($/MWHr) 131760 non-null float64
4 Marginal Cost Losses ($/MWHr) 131760 non-null float64
5 Marginal Cost Congestion ($/MWHr) 131760 non-null float64
dtypes: float64(3), int64(1), object(2)
memory usage: 7.0+ MBall_data**.**head()
对数据进行健全性检查。考虑到我们有 12 个月的数据,包括闰年的 2 月 29 日,是否加载了 366 天的数据?
**assert** file_counter **==** 366
有多少个区域,它们是什么?
unique_names **=** all_data['Name']**.**unique()
**print**(len(unique_names))
unique_names15
array(['CAPITL', 'CENTRL', 'DUNWOD', 'GENESE', 'H Q', 'HUD VL', 'LONGIL',
'MHK VL', 'MILLWD', 'N.Y.C.', 'NORTH', 'NPX', 'O H', 'PJM', 'WEST'],
dtype=object)
有多少排?
all_data**.**shape(131760, 6)
检查行数=区域数乘以一天 24 小时乘以一年 366 天:
**assert** 15*****24*****366 **==** all_data**.**shape[0]
对于这个例子,我们只关心纽约市。选择感兴趣的数据(特定区域):
zone_of_interest **=** 'N.Y.C.'
all_data **=** all_data**.**loc[all_data['Name']**.**isin([zone_of_interest]),:]all_data**.**shape(8784, 6)
有了DatetimeIndex
,DataFrame
会更容易使用。将索引重置为时间戳:
all_data **=** all_data**.**set_index(['Time Stamp'])
扮成datetime
:
all_data**.**index **=** pd**.**to_datetime(all_data**.**index, format**=**'%m/%d/%Y %H:%M')
让我们看看夏令时前后的数据:
start_time **=** pd**.**Timestamp(year**=**2019, month**=**11, day**=**2, hour**=**23)
end_time **=** pd**.**Timestamp(year**=**2019, month**=**11, day**=**3, hour**=**3)time_test_1 **=** all_data[start_time:end_time]
time_test_1
我们可以看到凌晨 1 点有两个条目,其中第二个条目是时钟“倒转”的结果。为了用这个DatetimeIndex
做算术,我们需要让它知道时区。Pandas 简化了这一过程,并适当地处理了重复的 1am 行:
all_data**.**index **=** \
all_data**.**index**.**tz_localize('America/New_York', ambiguous**=**'infer')time_test_2 **=** all_data[start_time:end_time]
time_test_2
现在我们可以看到,相对于 UTC 的偏移量已显示出来。在继续之前,让我们仔细检查一下数据的开头和结尾。
all_data**.**head()
all_data**.**tail()
看起来数据像预期的那样,从 2019 年 5 月 1 日到 2020 年 4 月底跨越了一年。
运行模拟
在本节中,我们将定义一个函数simulate_battery
,它模拟电池在一年中的能量套利操作。以下是该函数的输入:
initial_level
,模拟开始时电池充电的初始水平(kWh)price_data
、每小时 LBMP 的DataFrame
($/MWh)max_discharge_power_capacity
、 κ (千瓦)max_charge_power_capacity
,亦作 κ (kW)discharge_energy_capacity
(千瓦小时)efficiency
、交-交往返效率、、η (无单位)max_daily_discharged_throughput
、 τ (千瓦时)time_horizon
,优化时间范围(h),这里假设大于等于 24。start_day
,一只熊猫Timestamp
在第一个模拟日的中午
该函数返回几个可用于检查系统操作的输出:
all_hourly_charges
、all_hourly_discharges
、all_hourly_state_of_energy
、充电和放电活动以及能量状态,以每小时为时间步长(kWh)all_daily_discharge_throughput
、每日时间步长的排放吞吐量(kWh)
**def** **simulate_battery**(initial_level,
price_data,
max_discharge_power_capacity,
max_charge_power_capacity,
discharge_energy_capacity,
efficiency,
max_daily_discharged_throughput,
time_horizon,
start_day):
*#Track simulation time
* tic **=** time**.**time()
*#Initialize output variables
* all_hourly_charges **=** np**.**empty(0)
all_hourly_discharges **=** np**.**empty(0)
all_hourly_state_of_energy **=** np**.**empty(0)
all_daily_discharge_throughput **=** np**.**empty(0)
*#Set up decision variables for optimization by
* *#instantiating the Battery class
* battery **=** Battery(
time_horizon**=**time_horizon,
max_discharge_power_capacity**=**max_discharge_power_capacity,
max_charge_power_capacity**=**max_charge_power_capacity)
*#############################################
* *#Run the optimization for each day of the year.
* *#############################################
*
*#There are 365 24-hour periods (noon to noon) in the simulation,
* *#contained within 366 days
* **for** day_count **in** range(365):
*#print('Trying day {}'.format(day_count))
*
*#############################################
* *### Select data and simulate daily operation
* *#############################################
*
*#Set up the 36 hour optimization horizon for this day by
* *#adding to the first day/time of the simulation
* start_time **=** start_day \
**+** pd**.**Timedelta(day_count, unit**=**'days')
end_time **=** start_time **+** pd**.**Timedelta(time_horizon**-**1, unit**=**'hours')
*#print(start_time, end_time)
*
*#Retrieve the price data that will be used to calculate the
* *#objective
* prices **=** \
price_data[start_time:end_time]['LBMP ($/MWHr)']**.**values
*#Create model and objective
* battery**.**set_objective(prices)
*#Set storage constraints
* battery**.**add_storage_constraints(
efficiency**=**efficiency,
min_capacity**=**0,
discharge_energy_capacity**=**discharge_energy_capacity,
initial_level**=**initial_level)
*#Set maximum discharge throughput constraint
* battery**.**add_throughput_constraints(
max_daily_discharged_throughput**=**
max_daily_discharged_throughput)
*#Solve the optimization problem and collect output
* battery**.**solve_model()
hourly_charges, hourly_discharges **=** battery**.**collect_output()
*#############################################
* *### Manipulate daily output for data analysis
* *#############################################
*
*#Collect daily discharge throughput
* daily_discharge_throughput **=** sum(hourly_discharges)
*#Calculate net hourly power flow (kW), needed for state of energy.
* *#Charging needs to factor in efficiency, as not all charged power
* *#is available for discharge.
* net_hourly_activity **=** (hourly_charges*****efficiency) \
**-** hourly_discharges
*#Cumulative changes in energy over time (kWh) from some baseline
* cumulative_hourly_activity **=** np**.**cumsum(net_hourly_activity)
*#Add the baseline for hourly state of energy during the next
* *#time step (t2)
* state_of_energy_from_t2 **=** initial_level \
**+** cumulative_hourly_activity
*#Append output
* all_hourly_charges **=** np**.**append(all_hourly_charges, hourly_charges)
all_hourly_discharges **=** np**.**append(
all_hourly_discharges, hourly_discharges)
all_hourly_state_of_energy **=** \
np**.**append(all_hourly_state_of_energy, state_of_energy_from_t2)
all_daily_discharge_throughput **=** \
np**.**append(
all_daily_discharge_throughput, daily_discharge_throughput)
*#############################################
* *### Set up the next day
* *#############################################
*
*#Initial level for next period is the end point of current period
* initial_level **=** state_of_energy_from_t2[**-**1]
toc **=** time**.**time()
**print**('Total simulation time: ' **+** str(toc**-**tic) **+** ' seconds')
**return** all_hourly_charges, all_hourly_discharges, \
all_hourly_state_of_energy,\
all_daily_discharge_throughput
现在,我们将全年运行我们的模拟,使用以下电池参数的说明性值。
max_discharge_power_capacity **=** 100 *#(kW)* max_charge_power_capacity **=** 100 *#(kW)* discharge_energy_capacity **=** 200 *#(kWh)* efficiency **=** 0.85 *#unitless* max_daily_discharged_throughput **=** 200 *#(kWh)*
首先,我们假设电池充电到一半就可以启动。
initial_level **=** discharge_energy_capacity**/**2
initial_level100.0all_hourly_charges, all_hourly_discharges, all_hourly_state_of_energy,\
all_daily_discharge_throughput **=** \
simulate_battery(initial_level**=**initial_level,
price_data**=**all_data,
max_discharge_power_capacity
**=**max_discharge_power_capacity,
max_charge_power_capacity
**=**max_charge_power_capacity,
discharge_energy_capacity**=**discharge_energy_capacity,
efficiency**=**efficiency,
max_daily_discharged_throughput
**=**max_daily_discharged_throughput,
time_horizon**=**36,
start_day**=**pd**.**Timestamp(
year**=**2019, month**=**5, day**=**1, hour**=**12,
tz**=**'America/New_York'))Total simulation time: 20.976715087890625 seconds
健全性检查:模拟小时数应为:
**assert** 24*****365 **==** len(all_hourly_discharges)
分析电池运行
现在我们来看一组电池如何工作的指标。我们可以检查是否满足了所有的约束,并分析我们系统的财务影响。
功率输出
定义放电为正,充电为负的功率输出。
mpl**.**rcParams["figure.figsize"] **=** [5,3]
mpl**.**rcParams["figure.dpi"] **=** 100
mpl**.**rcParams**.**update({"font.size":12})plt**.**hist(all_hourly_discharges **-** all_hourly_charges)
plt**.**xlabel('kW')
plt**.**title('Hourly power output')Text(0.5, 1.0, 'Hourly power output')
这表明在一年中的大部分时间里,电力接近于零。换句话说,电池既不充电也不放电。然而,电池在其范围[-100,100] kW 的极限下运行也是常见的。
能量状态
在任何时候,电池的能量状态应不小于零,且不大于放电能量容量:[0,200] kWh。
plt**.**hist(all_hourly_state_of_energy)
plt**.**xlabel('kWh')
plt**.**title('Hourly state of energy')Text(0.5, 1.0, 'Hourly state of energy')
结果表明电池在规定的能量状态范围内工作。
收入、成本和利润
我们将分析以下财务指标:
- 年收入总额(美元)
- 年度总收费成本(美元)
稍后,我们还将了解总的年排放吞吐量(kWh)。为了检查所有这些,将数据放在一个DataFrame
中是很方便的。
根据模拟的时间范围选择一个新的DataFrame
,以报告进一步的结果:
all_data_sim_time **=** all_data[
pd**.**Timestamp(year**=**2019, month**=**5, day**=**1, hour**=**12, tz**=**'America/New_York'):
pd**.**Timestamp(year**=**2020, month**=**4, day**=**30, hour**=**11, tz**=**'America/New_York')]**.**copy()
检查行数是否正确:
all_data_sim_time**.**shape(8760, 5)**assert** all_data_sim_time**.**shape[0] **==** len(all_hourly_discharges)
附加模拟结果
*#These indicate flows during the hour of the datetime index* all_data_sim_time['Charging power (kW)'] **=** all_hourly_charges
all_data_sim_time['Discharging power (kW)'] **=** all_hourly_discharges
all_data_sim_time['Power output (kW)'] **=** \
all_hourly_discharges **-** all_hourly_charges
*#This is the state of power at the beginning of the hour of the datetime index* all_data_sim_time['State of Energy (kWh)'] **=** \
np**.**append(initial_level, all_hourly_state_of_energy[0:**-**1])
收入和成本的单位是
所以除以 1000 调整为美元:
all_data_sim_time['Revenue generation ($)'] **=** \
all_data_sim_time['Discharging power (kW)'] \
***** all_data_sim_time['LBMP ($/MWHr)'] **/** 1000all_data_sim_time['Charging cost ($)'] **=** \
all_data_sim_time['Charging power (kW)'] \
***** all_data_sim_time['LBMP ($/MWHr)'] **/** 1000all_data_sim_time['Profit ($)'] **=** all_data_sim_time['Revenue generation ($)'] \
**-** all_data_sim_time['Charging cost ($)']
年总收入是多少?
all_data_sim_time['Revenue generation ($)']**.**sum()2354.6574498602467
每年总充电成本?
all_data_sim_time['Charging cost ($)']**.**sum()1391.6754123382877
计算利润
all_data_sim_time['Profit ($)']**.**sum()962.9820375219592
因此,我们可以通过执行能源套利获得近 963 美元的利润。
总年排放吞吐量
一年中有多少能量流过这个电池?在某些情况下,每天的排放总量限制在 200 千瓦时/天。如果电池在 365 天的模拟过程中每天都释放其最大可能能量,则总放电量为:
365*****200
*#kWh*73000
事实上它是:
sum(all_daily_discharge_throughput)72955.00000394997
这意味着系统在大多数日子里达到了最大排出吞吐量限制。我们可以通过对日吞吐量的Series
进行value_counts()
来检查这一点。
pd**.**Series(all_daily_discharge_throughput**.**round(0))**.**value_counts()200.0 364
155.0 1
dtype: int64
除了一天之外,电池一直以最大生产能力运行。
找到最赚钱的一周
按周对利润列进行分组,并找到最大值:
max_profit_week **=** (all_data_sim_time['Profit ($)']**.**resample('W')**.**sum() **==** \
all_data_sim_time['Profit ($)']**.**resample('W')**.**sum()**.**max())**.**valuesall_data_sim_time['Profit ($)']**.**resample('W')**.**sum()[max_profit_week]Time Stamp
2019-07-21 00:00:00-04:00 51.015471
Freq: W-SUN, Name: Profit ($), dtype: float64
7 月的一周是能源套利最有利可图的一周。本周,让我们制作一个每小时电池能量状态和每小时 LBMP 的图表。
mpl**.**rcParams["figure.figsize"] **=** [8,6]
mpl**.**rcParams["figure.dpi"] **=** 150
mpl**.**rcParams**.**update({"font.size":14})most_profit_week_start **=** pd**.**Timestamp(
year**=**2019, month**=**7, day**=**21, tz**=**'America/New_York')
ax **=** all_data_sim_time[
most_profit_week_start:most_profit_week_start**+**pd**.**Timedelta(weeks**=**1)]\
[['State of Energy (kWh)', 'LBMP ($/MWHr)']]\
**.**plot(secondary_y**=**'LBMP ($/MWHr)', mark_right**=**False)
ax**.**set_ylabel('State of energy (kWh)')
ax**.**right_ax**.**set_ylabel('LBMP ($/MWh)')
ax**.**get_legend()**.**set_bbox_to_anchor((0.3, 1))
电池似乎遵循“低买高卖”的一般套利策略,或者在这种情况下,“便宜充电,谨慎放电”,以利用未来的价格变化。可能是在这一周,纽约市相当温暖,导致空调电力需求高,昼夜价格波动大,也是我们的电池系统赚钱的好机会。
月收入
mpl**.**rcParams["figure.figsize"] **=** [6,4]
mpl**.**rcParams["figure.dpi"] **=** 100
mpl**.**rcParams**.**update({"font.size":12})all_data_sim_time['Profit ($)']**.**resample('M')**.**sum()**.**plot()
plt**.**ylabel('Total monthly profit ($)')Text(0, 0.5, 'Total monthly profit ($)')
夏季的利润通常高于冬季,但 1 月和 12 月的利润也很高。LBMP 全年的一幅图对此有所启发:
all_data_sim_time['LBMP ($/MWHr)']**.**plot()
plt**.**ylabel('LBMP ($/MWHr)')Text(0, 0.5, 'LBMP ($/MWHr)')
价格的变化使得套利策略能够获利。一般来说,大多数较大的价格波动发生在夏季,这可能反映了炎热夏季空调使用量的增加。但是 11 月、12 月和 1 月的价格变化很大。这可能是由于纽约市在假日期间的旅游业或寒冷的天气增加了对取暖用电的需求。
结论
我们发现,一个并网电池的能源套利策略可以使用线性规划来制定,假设未来价格在一定时间范围内是已知的。我们表明,当在一组说明性的系统参数下运行并使用真实世界的能源价格数据时,这样一个系统每年可以产生 963 美元的利润。
如果价格能够在这里使用的 36 小时优化范围之外准确预测,则可能会进一步优化以增加利润。NYISO 价格的确定涉及一个负荷预测模型,该模型取决于经济和天气因素。有可能在价格预测模型中包括这些因素,以估计尚未公开的未来一天的市场价格。在另一个有趣的方向上,王和张(2018)表明,使用历史价格数据的强化学习可以比最大化瞬时利润带来更高的利润,这表明了从能源套利中最大化利润的其他可能方法。
我希望这篇文章有助于你理解在未来价格已知的情况下,如何使用线性规划来制定最优套利策略。
参考
所有引用都是在 2020 年 5 月 2 日访问的。
尼索。日前调度手册。
PJM 互联有限责任公司。区位边际定价成分。
Salles,Mauricio B. C .等人,2017 年。PJM 储能系统潜在套利收益。能量 10:8。
Sioshansi,Ramteen 等人,2009 年。估算 PJM 电力储存的价值:套利和一些福利效应。能源经济学 31:2,269–277。
王,郝,张,2018。通过强化学习在实时市场中进行储能套利。IEEE PES 大会。
我发现这本由 Ben Alex Keen 撰写的指南非常有帮助。
原载于 2020 年 5 月 3 日 https://www.steveklosterman.com。
用弹性搜索设计最佳多语言搜索引擎
设计多语言弹性搜索索引的四种不同方法
乔尔·那仁在 Unsplash 上拍摄的照片
当我为 NewsCatcherAPI 设计 Elasticsearch index 时,我遇到的最大问题之一是处理多语言新闻文章。
我知道 Elasticsearch 为最流行的语言预建了分析器。问题是“我如何管理不同语言的文档,以便可以一起搜索(如果需要的话)?”
重要提示:在我们的例子中,我们已经用正确的语言标记了每个文档。尽管如此,这并不是本帖中描述的所有方法都必须的。
同样,对于这个帖子设置,让我们假设每个文档(新闻文章)只有两个字段:**title**
和**language**
。其中**language**
是标题的语言。为了简单起见,假设只能有两种不同的语言:英语(**en**
)和法语(**fr**
)。
为什么要关心语言呢?
每种语言在很多方面都不同(我会说 4 种语言,所以给我一些学分)。词汇化、词干化、停用词。所有这些在每种语言的基础上都是独一无二的。
所以,如果你想让 Elasticsearch 明白“dogs”只是“dog”的复数形式,或者“different”和“different”同根同源——你必须使用语言特定的分析器。(甚至对于英语来说!)
首先,我将描述我在网上找到的两种方法,并解释为什么我不喜欢它们。然后,我将提出我的解决方案,我们曾在新闻发布会上使用过。最后,我会留下一个链接,链接到一个非常先进的方法,可以自动检测语言。
方法 1。多领域
这种方法的思想是使用[**fields**](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/multi-fields.html)
参数多次索引文本字段。
例如,如果我们想创建一个索引,用标准、英语和法语分析器索引同一个文本字段:
PUT your_index_name_here
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"en": {
"type": "text",
"analyzer": "english"
},
"fr": {
"type": "text",
"analyzer": "french"
}
}
}
}
}
}
因此,**title**
变量被索引 3 次。现在,要搜索所有语言,您必须执行多匹配搜索。例如:
GET your_index_name_here/_search
{
"query": {
"multi_match": {
"query": "ce n'est pas une méthode optimale",
"fields": [
"title",
"title.en",
"title.fr"
],
"type": "most_fields"
}
}
}
多字段方法的优势
- 易于实施
- 即使数据没有标注语言,也能正常工作
多字段方法的缺点
当你只有两种语言时,这是可以接受的,但是假设你有 10 种语言(就像我们一样)。
- 索引时间较慢。每种语言的索引
- 更多存储空间。每个语言索引占用存储空间
- 昂贵的查询。在阅读更多关于我的另一篇文章
前两点可能没那么糟,然而,第三点就糟了。假设你有 10 种语言。要搜索整个数据库,您必须编写一个多匹配查询,该查询必须同时搜索 10 个不同索引的字段(加上索引碎片的数量)。
综上所述,对于 2-3 种语言的索引(和宽松的预算),这可能是一个可接受的选择。
方法二。多指数
在 Stackoverflow 上可以得到的最流行的答案(假设每个文档的语言在索引前是已知的)。
为每种语言创建单独的索引。例如,一个英文文本的索引我们称之为,法文文本的索引为。
然后,如果你知道搜索的语言,你可以把它导向正确的索引。
多指数方法的优势
- 不会多次存储相同的信息
多指数方法的缺点
- 管理多个指数。另外,不同语言的文档不会遵循统一的分布
- 从集群的角度来看,索引并不是免费的,因为每个索引都有一定程度的资源开销。
- 需要通过所有索引来搜索公共字段。
关于最后一点。假设我们有一个时间戳字段,我们想检索本周发表的所有文章。为此,您必须在所有索引上按发布的日期时间字段进行筛选。从技术上讲,这根本不是问题,只需通过通配符从多个索引中搜索你的字段。
例如,如果我们想同时搜索和,只需使用。
但是,这并不比使用多匹配搜索更好。现在是多索引搜索。
方法三。使用摄取处理器识别正确的字段
我的策略如下:
- 创建一个索引
- 对于每种语言,创建它自己单独的字段(不是子字段)
- 设置摄取处理器,根据语言参数值设置
**title_{lang}**
字段
每种语言都有一个单独的字段
PUT your_index_name_here
{
"mappings": {
"properties" : {
"language" : {
"type" : "keyword"
},
"title" : {
"type" : "text"
},
"title_en" : {
"type" : "text",
"analyzer" : "english"
},
"title_fr" : {
"type" : "text",
"analyzer" : "french"
}
}
}
}
我们的源数据没有 nor 字段。因此,我们必须设置摄取节点的管道来解决这个问题。
摄取节点
根据官方文件:
在实际的文档索引发生之前,使用摄取节点预处理文档。摄取节点拦截批量和索引请求,应用转换,然后将文档传递回索引或批量 API。
我们将使用一个设置处理器根据**language**
值将**title**
值“复制”到**title_en**
或**title_fr**
。
我们必须编写一个简单的无痛脚本来使 set 处理器有条件。
我们创建了一个名为" langdetect "的接收管道
PUT _ingest/pipeline/langdetect
{
"description": "copies the text data into a specific field depending on the language field",
"processors": [
{
"set": {
"if": "ctx.language == 'en'",
"field": "title_en",
"value": "{{title}}"
}
},
{
"set": {
"if": "ctx.language == 'fr'",
"field": "title_fr",
"value": "{{title}}"
}
}
]
}
它有 2 个处理器,将根据**language**
字段的值设置**title_en**
和**title_fr**
字段。
根据我们的流水线,如果**language**
字段的值等于“en ”,那么将**title_en**
字段设置为**title**
字段中的值。因此,英语文本将由标准分析器(**title**
字段)和英语分析器(**title_en**
字段)进行分析。
现在,当创建管道时,我们必须将它“附加”到索引上。因此,让我们更新我们的索引设置:
PUT /your_index_name_here/_settings
{
"default_pipeline" : "langdetect"
}
优势
- 支持多语言的单一索引
缺点
- 较慢的索引时间
- 仅在语言已知时有效
对于 NewsCatcherAPI 的例子,当用户想要用英语搜索时,她必须在我们的 API 中将语言参数设置为**en**
。我们,在后端,将通过**title_**
+ **{lang}**
进行搜索,在**en**
的情况下是**title_en**
。这比那要复杂一点,但应该足以解释这篇博文。
方法 4。检测 Elasticsearch 中的语言,然后进行适当的索引
结论
当我意识到用 Elasticsearch 管理多语言搜索索引并不容易且显而易见时,有点惊讶。我不得不花很多时间来找出我们用例的最佳情况。
希望这份“小抄”能为你节省一点时间!
如果有不清楚的地方,请在评论中提问。
顺便说一下,如果你需要帮助你的 Elasticsearch 集群/索引/设置,我会提供咨询。
artem[at]news catcher API[dot]com
原载于https://codarium.substack.com。
基于遮挡的人脸检测器的设计与开发
“增强安全性的最佳方式是通过面部识别——它很快就会成为标准。”—凯莎·威廉姆斯
嗨,大家好,我又带来了另一篇文章,但这次它不是基于任何理论,而是关于开发一个现实世界问题的应用程序。
所以,我希望你们都感到兴奋,因为今天我们将学习开发一个成熟的人脸检测系统,它可以处理遮挡的人脸以及其他场景。但是等等,等等…被遮挡的脸是什么意思?被遮挡的脸是指不完全可见的脸,我的意思是它的一部分被一些东西覆盖着,如面具或相机由于遮挡或其他原因无法捕捉到脸部的镜头。
我们为什么需要这个?嗯,这可以在捕捉某人进行一些邪恶的活动或其他事情时找到很多应用。
所以我们的要求是:
- 计算机编程语言
- 打开简历
- 人脸识别模块。
我相信你们都非常熟悉前两个,并且随着我们的深入了解第三个。
这是我们的文章提纲。
- 我们将与图像一起使用的文件
- 正在生成编码。
- 使用人脸检测模型检测人脸[图像+视频]
- 结论。
来源:负空间 Pexel
要使用的文件和图像
我们将维护两个名为 gen_encoding.py 的文件,用于生成编码和检测人脸的人脸识别。
我们将使用我们想要检测其面部的人的图像。如果能给我们几张照片就更好了。我们将把这些人的图像存储在一个单独的文件夹中,该文件夹以图像所属的人的名字命名。
生成编码。
import opencv
import face_recognition
import pickle
import cv2for i,image_p in enumerate("image directory path"):
image =cv2.imread(image_p))
name = image_p.split(".")[0]
在上面的代码片段中,我们在顶部导入所需的库,并使用 for 循环从图像目录中串行读取图像,并通过分割图像扩展名来获取图像的名称。
rgb = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
boxes = face_recognition.face_locations(rgb,model="cnn")
encodings = face_recognition.face_encodings(rgb,boxes)
在上面的代码片段中,我们根据 face_recognition 库中的模型来转换图像的颜色。
在第二行中,我们从 face_location 函数中获取图像的位置,该函数返回维数的平方。
进一步使用这些位置,我们裁剪出面部区域,并使用 face_recognition 的 face_encodings 功能生成图像编码,该功能使用了特定的神经网络模型来完成一些工作,但我不会详细介绍它,因为它与上下文无关。
encodings=[], known_names= []
for encoding in encodings:
#Processsing the encodings known
encodings.append(encoding)
known_names.append(name)
在上面的代码中,我们将图像的编码和名称存储在一个列表中,并将它们存储在一个 pickle 文件中,如下所示。
data = {"encoding": known_encodings, "names":known_names}
f =open("encoding file name","wb"):
f.write(pickle.dumps(data))
使用人脸检测模型检测人脸。
现在编码部分已经足够了。现在,我们将与检测脸的真实切片一起工作。
为了编写我们的代码,我们将使用 face_recognition.py 命名文件。
在下面的代码中,我们将打开如上生成的编码文件,并将其存储在数据变量中。
data = pickle.loads(open(args["encoding"],"rb").read())
用图像检测
在本节中,我们将在提供的图像中进行人脸检测。因此,首先我们将加载图像,并将其转换为标准的颜色格式,我们将生成它的编码进行比较。
image = cv2.imread(os.path.join("image_path"))
rgb = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
boxes= face_recognition.face_locations(rgb,model= "cnn")
encodings = face_recognition.face_encodings(rgb,boxes)*Detection with vidoes*
现在,在下面的代码片段中,我将图像生成的编码与所有已经存储的人脸编码进行比较。如果它们之间的差异低于阈值,我们将其作为相应人脸的编码并用 imshow()函数显示。
for encoding in encodings:
matches=face_recognition.compare_faces(data,encoding)
name = "Unknown"
if True in matches:
match_idx = [i for (i,b) in enumerate(matches) if b]
counts= {}
for i in match_idx:
name = data["names"][i]
counts[name] =counts.get(name,0)+1
name = max(counts, key= counts.get)
names.append(name)
for ((top,right,bottom,left),name) in zip(boxes,names):
cv2.rectangle(image,(left,top),(right,bottom),(0,255,0),2)
y = top-15 if top-15>15 else top+15
cv2.putText(image,name,(left,y),cv2.FONT_HERSHEY_SIMPLEX,0.45,
(0,255,0),2)
cv2.imshow("Image",image)
视频帧中的检测
现在重复几乎相同的过程,现在是视频帧而不是图像。
在下面的代码中,我们正在初始化我们电脑的网络摄像头。
vs= VideoStream(src=0).start()
在下面给出的代码中,我们只是读取每一帧,并根据 face_recognition 库中的模型将其转换为标准颜色格式。
现在,我们使用 face_location 函数检测视频帧中的人脸位置,并在代码的最后一行生成其编码。
while True:
frame = vs.read()
rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
rgb = imutils.resize(rgb,width=750)
r =frame.shape[1]/float(rgb.shape[1])
boxes= face_recognition.face_locations(rgb,model="cnn")
encodings =face_recognition.face_encodings(rgb,boxes)
names=[]
现在,在下面的代码中,我们将帧的编码与已经存储的人脸编码进行比较,如果我们发现差异低于某个阈值,则认为该帧包含编码已经存储的人的人脸。
for encoding in encodings:
matches=
face_recognition.compare_faces("encoding.pickle",encoding)
name="Unknown" print(matches)
if True in matches:
matchesIdx =[i for (i,b) in matches if b]
counts={}
for i in matchesIdx:
name = data["names"][i]
counts[name]= counts.get(name,0)+1
name =max(counts,key= count.get)
names.append(name)
现在,在下面的代码中,我们只是在脸部周围画一些方框,并在上面显示这个人的名字。
for((top,right,bottom,left),name) in zip(boxes,names):
top=int(top*r)
right=int(right*r)
bottom=int(bottom*r)
left=int(left*r)
cv2.rectangle(frame,(left, top),(right, bottom),(0,255,0),2)
y=top-15 if top-15>15 else top+15
cv2.putText(frame,name ,(left,y),cv2.FONT_HERSHEY_SIMPLEX,0.75,
((0,255,0),2)
cv2.destroyAllWindows()vs.stop()
万岁…你做到了。恭喜你开发出第一个高级人脸识别模型。
结论:
虽然代码乍一看似乎很复杂,但整个代码背后的概念非常简单。好吧,让我给你简单介绍一下我们所做的一切。首先,我们为所有可用的图像生成了编码。现在,在检测时,我们只是将正在检测的人的编码与已经存储的人的编码进行比较。所以很明显同一个人会表现出较少的差异。如果差异没有超过阈值,我们会将该人脸标记为未知人。简单!!
完整代码可以访问我的 Github 简介
如果你喜欢的内容,并希望更多这样的,跟着我一样。
谢谢你
设计数据密集型应用程序
数据库和分布式系统用户的现代经典
意识到自己对某件事知之甚少可能是一次令人沮丧的经历。然而,这本书设法使它振奋人心,引人入胜。在设计数据密集型应用中,Martin Kleppmann 首先解释了简单数据库的工作原理,然后介绍了分布式环境中多个系统的交互方式。在这一过程中,他采用了许多我认为我理解的概念,并展示了我从来不知道的深度和复杂性,以提供更彻底的理解。
作者图片
例如,数据库中的 ACID (原子性、一致性、隔离性、持久性)事务(在第七章中介绍)。我已经看过这个缩写一百万次了,我想我已经很好地理解了它。我以为数据库要么提供要么不提供,但你知道吗,它实际上是令人惊讶的模糊?克莱普曼甚至称它主要是一个营销术语。你知道一致性不属于其他人吗?这种隔离实际上可以通过许多不同的严格程度来实现?Kleppmann 在第 7 章中以清晰和吸引人的方式解释了这一切。我觉得我得到了关于数据库如何真正运行的内部消息。
书籍内容
这本书有 12 章,分成 3 个部分。
- 数据系统的基础:数据库实际上如何存储数据,索引和它们如何更新,使用的文件类型。
- 分布式数据:跨多个节点的数据复制、分区、事务的实际执行方式。
- 派生数据:批处理(包括 MapReduce)和流处理。
这些章节是建立在彼此的基础上的,但是你可以浏览你特别感兴趣的章节。如果他提到了书中以前的主题,他会附上更多信息的页码,这样你就不会感到失落。
每一章都以像这样的古怪地图开始。图片作者。
这本书里没有练习,只有几个代码示例。它描述了问题以及如何实现解决方案,但是你不会获得任何解决这些问题的经验。只期望提高你的意识,增加你的理解。
他引用了从 20 世纪 70 年代到 2016 年的数据库和计算历史的大量资料(在某些章节中超过 100 篇)。它们来自书籍和论文,但也有博客文章,甚至黑客新闻聊天讨论(参考第 11 章的 61)。这种多样性,以及它们不都是学术参考,有助于增强你了解全貌的信心。
一路上有一些有趣的花絮。就像 MapReduce 作业中的每个子任务在每一步之间都会写入磁盘,这看起来就像最初开发它的 Google 设计人员过度担心硬件故障一样。但是,如果您知道它的初衷是在有空闲资源时在后台运行作业,这就更有意义了。显然,一个小时长的 MapReduce 任务有 50%的机会被终止,因此计算资源可以被更高优先级的作业使用。
不好的地方
关于这本书,我要说的唯一不好的一点是,它最初是在 2017 年初出版的,年龄开始显示出来了。VoltDB 和 Riak 是两个获得最频繁引用的数据库,但我从未听说过它们。从谷歌趋势来看,自 2015 年以来,它们的受欢迎程度一直在下降(已经是小众的了)。
这本书从 2010 年开始引用了很多关于分布式数据库、批处理和流处理的有趣内容,但这些内容在 2016 年停止了。我很想知道他在过去 4 年中会包括什么,因为这是一个发展迅速的领域。
谁应该读它?
我建议有一些数据库或软件开发的经验,以便看到材料的相关性。这是一本相当厚的书(550 页左右),我花了 45 个小时才读完,所以你需要愿意花一些时间来读完它。也就是说,我相信这绝对是值得的时间投资。
结论
你会从阅读中受益吗?我做得很快。这篇关于 Google Spanner 分布式数据库的中型文章最近出现在我的 feed 中。它提到了故障切换、事务、一致性、防止过时读取、跨区域复制等内容。在读这本书之前,我要么不知道这些概念是什么,要么不理解它们的复杂性。所以现在我受益于能够更好地理解和消化这样的帖子。
评级:🦅🦅🦅🦅🦅
5 鹰是这本书唯一合适的评级。它以全新的视角向你展示一个熟悉的世界的能力,只有 5 只老鹰把你从椅子上拉起来,带你在你的城市上空翱翔才能与之媲美。
数据产品和客户
从客户的角度出发,通过逆向工作提高影响力
Mike Akeroyd(作者)在 Blender 中渲染的抽象背景图像。
在过去的 10 年里,根据谷歌趋势数据,“机器学习”和“数据科学”的搜索量增加了 12 倍。随着流行,可能会出现一种非理性的兴奋,公司可能会为了进行机器学习而想要进行机器学习。构建由机器学习驱动的产品需要一种微妙的平衡,既要提供早期的增量价值来满足业务合作伙伴,又不能通过在模型中使用太少的数据或功能来淡化影响。作为产品经理、科学经理或科学家,我们需要管理内部期望,同时保持对 ML 计划的支持势头。要做到这一点,我们应该关注客户需求,而不是只关注科学成果。
但是谁是科学产品的客户呢?如果我们正在开发一个客户流失倾向模型,那么客户可能是一个保留营销团队。如果我们正在建立一个价格弹性模型,那么客户可能是一个产品价格策略团队。在另一个例子中,如果我们推出一个网站聊天机器人,那么客户可能是一个数字支持团队。然而,在所有这些情况下,这些机器学习模型的“客户”也可能是最终的商业客户:保留促销的接收者,愿意为产品支付价格的人,或者向聊天机器人提问的用户。微妙的区别在于特定机器学习模型或数据产品与最终商业客户的直接互动水平。数据产品的早期版本可以产生建议,并且内部用户可以实施该建议。一个更高的版本可以完全自动地与最终的商业客户进行交互。然而,仅仅因为一个数据产品最初的范围是支持内部用户,未来的发展可能会产生一个直接与最终业务客户交互的数据产品。因此,我们应该始终从最终客户“向后工作”。
有几篇文章提供了如何思考数据产品或任何使用数据来传递其核心价值主张的产品的想法。根据这个定义,任何使用机器学习的产品都可以被认为是数据产品。一篇文章 [1]将数据产品分为三种类型:数据即服务、数据即洞察和数据增强产品。另一个【2】也将数据产品分为三种:标杆数据、预测数据、推荐系统数据。另一个文档【3】将数据产品分为:原始数据、派生数据、算法、决策支持和自动化决策。在每一种情况下,数据产品都根据产品的产出进行分类。本文将介绍一个新的基于客户的数据产品分类框架,而不是关注输出。这个框架允许我们更容易地考虑与数据产品开发相关的技术和组织约束。
从客户开始向后工作有助于我们考虑基于客户需求的增量开发。它还允许我们设定价值主张的期望值。
在我们开始之前,这里有一个框架概述。
离顾客三度…
"三级数据产品"通过两层或更多层的人类互动或其他科学驱动的输入与最终客户分离。客户聚类、离线欺诈评分、产品评论情绪或保留概率模型的输出都是三级数据产品的示例。这种类型的数据产品以数据集、科学笔记本或可视化的形式存在。这是从数据中获得洞察力的核心。没有这些被科学家和分析师消费的数据产品,就不可能有任何已实现的未来价值。作为一个科学团队,第三度数据产品通常是最令人兴奋的讨论(“我们要预测 x!”)但从业务角度来看,提供支持可能会很困难。这是因为通常很难衡量这类产品带来的直接财务影响。然而,第三度数据产品提供了真正的选择价值。这种真正的选择是扩展到具有可测量价值的一级或二级数据产品的潜力。通常,实物期权的价值非常重要,因为一个三级数据产品可以支持许多领域。例如,客户终身价值模型可以支持用于市场细分、促销目标或客户支持队列优先级排序的附加数据产品。
与顾客相距两度……
“二度数据产品”与终端客户之间隔着一层人机交互。这些产品通常由内部业务团队使用,可以包括包含预测和优化的 ui。二度数据产品不仅仅是见解,而是建议。这些建议还可以包括具有敏感性分析的替代方案。有时,这些产品可以开发为原型或第一个版本,随后会产生第一级数据产品。在其他情况下,我们可能永远无法获取做出整体决策所必需的适当数据,因此始终需要对基于科学的建议施加“人为影响”。在另外一些情况下,如果产品支持的决策很少被做出,并且影响相当大,那么让产品变得更不干涉可能会有很大的风险。与二级数据产品相关的量化值可能是模糊的。这是因为人们需要回答这样一个问题:做出更明智决策的价值是什么?该值是与科学建议和人类决策相关的影响的混合。二级数据产品的主要风险是缺乏采用。如果推荐的决策无法解释,不直观,或者需要大量的人工干预,那么产品很可能会被弃用。
离客户 1 度…
我们可以将机器学习模型的输出直接与最终客户接触的产品分类为“一级数据产品”。这意味着 ML 输出与用户仅相差一度。这些数据产品的例子包括个性化用户体验、产品推荐、聊天机器人和驾驶员导航。有了这些类型的数据产品,我们可以通过 AB 测试来评估客户影响和衡量业务价值。从业务角度来看,量化价值的能力使这些类型的数据产品更容易划分优先级。然而,由于这些产品与客户互动,总会有损害客户信任的风险。例如,第一级数据产品可能提供不相关的建议、不正确的驾驶方向、极端的价格变化,或者在 IBM 的 Watson 的情况下,向医生提供错误的癌症治疗建议。推出一级数据产品需要工程支持,并且必然存在许多依赖关系。通常,需要一个适应性强的 UX 来支持实验和自动数据收集。对于这些数据产品,我们可以使用业务规则引导早期实验,收集的数据稍后用于支持模型训练和增强。随着时间的推移,随着该产品从客户那里收集直接反馈,机器学习模型可以提供更相关的输出。
区分数据产品和科学模型……
让我们快速浏览一个例子,看看这个框架在实践中是如何工作的。在这个例子中,我们为一家处于分析之旅早期的公司工作,我们的任务是使用机器学习来减少客户流失。最终的第一级数据产品的“最终客户”是可能流失的客户。这种产品可以是一个系统,它可以自动识别具有高流失倾向的客户,并向他们发送有针对性的促销优惠。达到这一点不仅需要科学工作,也需要工程工作。因此,实现产品的价值需要时间,如果有任何开发延迟,这可能会增加失去管理层支持的风险。当我们建立客户级别的保留概率模型来驱动这个一级数据产品时,我们也可以提供二级数据产品。这种二级数据产品可以是“防止流失目标工具”,营销人员可以使用它来识别具有高流失可能性分数的客户。有了这些信息,营销团队可以手动提供促销以降低流失风险。通过分解交付,组织可以更快地实现价值,并可能通过实验来确定客户流失预测的有效性。最后,第三度数据产品是客户流失科学模型的输出。该输出不仅可以支持我们减少客户流失的目标,还可以作为其他科学模型的输入。例如,客户流失或保留概率模型可以是客户终身价值模型的输入。
示例数据产品分类
所以,概括一下:
一级数据产品直接向客户提供基于科学的输出,供他们采取行动。
二级数据产品向企业提供基于科学的输出,供员工采取行动,然后提供给客户。
第三度数据产品向第二度数据产品提供基于科学的输出。第三级数据产品也可以向分析师或科学家提供见解,而不提供建议。
从数据产品中实现价值有时可能是一个漫长的旅程:对客户和企业都是如此。照片来自 Pexels 由flo·马德雷纳拍摄
最后一点要注意的是,我们可以进一步扩展这个框架,以包括多一种数据产品类型:“零度数据产品”。零度数据产品使用其基于科学的输出来代表客户采取行动。这不同于第一级数据产品,第一级数据产品要求客户仍然自己行动。零度数据产品的例子包括无人驾驶汽车、无人驾驶飞机或其他自主机械。这种类型的数据产品与提到的其他三种产品之间有一个关键的技术差异。零度数据产品往往是用深度学习构建的。相比之下,我们通常使用机器学习算法来构建其他三类数据产品。
总之,当开发一个人工智能战略或建立一个 ML 项目路线图时,很容易变得过于雄心勃勃。在讨论 ML 解决方案时,组织通常会设想一级数据产品。实际上,许多数据产品需要从三级数据产品开始,并逐渐变得更加复杂。这种以客户为中心的数据框架不是保证高质量科学产品的灵丹妙药,但它可以帮助我们定义产品。它还帮助我们理解如何从敏捷开发的角度来思考。这样做,我们可以最小化范围蔓延,同时首先向内部业务合作伙伴提供价值,然后向外部客户提供增量交付。
参考文献:
[1]https://www . mind the product . com/fundamentals-building-better-data-products/
[3]https://towards data science . com/designing-data-products-b 6 b 93 EDF 3d 23
设计数据库存储和检索
关于数据库存储、索引和数据检索,您需要知道的一切
数据库是任何应用程序设计中不可或缺的一部分,进程在其中存储和管理数据。虽然,您可能永远不需要从头开始设计数据库,但是了解其设计以及不同数据库处理数据的方式将有助于您选择最合适的数据存储,从而提供所需的可伸缩性和性能。例如,适合处理事务性数据的数据库可能不适合数据仓库。
让我们开始设计一个数据库。我们将从简单开始,然后优化它,直到它变得可怕。
最简单的方法是将键值对保存在一个文件中。写入记录只会将记录附加到文件末尾。如果某个段(或文件)的大小超过了最大可能大小,则开始写入下一个段。作为后台进程,这些段被合并以移除重复的条目并保留关键字的最新值。这个过程叫做压实。这种方法的唯一问题是,read 必须解析整个段才能找到用那个键写入的最后一个值。这些可能进一步涉及在多个段中搜索,导致读取时间增加。让我们试着解决这个问题。一种方法是在键上建立索引。
哈希索引
创建索引的一个简单方法是使用哈希映射。你可以有一个内存中的哈希表来存储键和字节偏移量,即它在该段中的位置。每个数据段都有自己的内存哈希映射。
在散列索引中查找
写操作将在哈希表中创建一个条目,并将记录追加到最新的段中。读取将涉及到遍历散列表,以找到要读取的记录的位置。
这种方法有一些缺点:
1。索引保存在内存中。当键的数量很大并且超过可用的内存时,这是没有用的。
2。它不是为范围查询而优化的。
桌子和 LSM 树
排序字符串表(SSTables)包含按键排序的数据,一个键在一个段中只出现一次。有了有序排列的键,就不需要在哈希表中索引每个键,只需要索引少数记录的偏移量。使用排序数据段的其他好处是,它允许在写入前压缩数据块,并且使压缩过程更容易。可以使用简单的合并排序算法来合并段。
表中的索引和压缩
下一个问题是如何在 segment 中保持键的排序顺序?使用像 AVL 这样的自平衡树,在内存中存储排序结构要容易得多。
- 当写入到来时,将其添加到内存平衡树(称为 memtable )。
- 随着内存的增长,从 memtable 中读取值并将其写入磁盘。因为它已经排序,段将有排序的关键字。
- 在读取期间,首先检查 memtable,然后检查最近的段。
但是如果我们的系统在将 memtable 写到磁盘之前崩溃了呢?要解决这个问题,请使用日志。为磁盘中的每次写入保留一个日志。如果系统崩溃,这些可以用来构建段。
日志结构树或 LSM 树是一种数据结构,旨在为长期经历高插入(和删除)率的文件提供低成本索引。它基于在后台合并的表的思想。不同的段可能包含相同关键字的值。这些数据段在后台进行压缩,以删除重复数据并节省空间和读取时间。
b 树
B-tree 是一种自平衡的树形数据结构,用于维护已排序的数据。在大多数其他自平衡搜索树(如 AVL 树和红黑树)中,假设所有东西都在主存中。为了理解 B 树的使用,想象一下在主存中容纳不下的大量数据。当键的数量很大时,数据以块的形式从磁盘中读取。与主内存访问时间相比,磁盘访问时间非常长。使用 B 树的主要思想是减少磁盘访问的次数。对数时间内的 b 树搜索、顺序访问、插入和删除。你可以在这里阅读更多关于 B 树的内容。
像表一样,树也保持键的顺序。唯一的区别在于设计理念。LSM 树是将数据写入不同段的日志结构索引,而 B 树是一次仅在一个页面(或磁盘块)中读取或写入的页面结构索引。它将数据存储分为固定大小的块或页面。叶节点可以包含键的实际值,或者指向存储该值的引用。下面的图片将帮助你理解 B 树是如何工作的。
在 B 树中查找 key = 272
读取时,需要遍历树,找到存储块的位置。在写入时,它首先需要找到需要写入值的页面,然后写入。如果块中没有足够的空间,可能会导致块分割和对父节点的更新。
为了使写入对崩溃具有弹性,它使用日志。在任何写操作之前,首先将它记录在磁盘中,然后将其插入到 B 树中。
比较
- 读/写性能:像 LSM 树这样的日志结构索引允许更快的写入,而像 B 树这样的页面结构索引允许更快的读取。LSM 中的读取相对较慢,因为它们需要根据压缩级别检查多个数据结构和表。
- 冗余:对于 B 树,每个键只存储在一个地方,而对于日志结构,它存储在多个段中。
- 存储 : B 树由于碎片化 ie 可能会留下一些磁盘空间。当前行无法放入可用页面空间时。LSM 树的压实过程减少了碎片。
- LSM 需要优化的压缩策略来合并数据段,以获得更好的读取性能。压缩策略不应影响正在进行的读/写操作。当写入吞吐量较高,并且正在进行的写入和执行压缩的线程之间共享有限的磁盘带宽时,这可能会导致问题。
b 树更受欢迎,用于维护索引。
所有其他索引,如二级索引、多列索引等都扩展了上述方法。唯一的区别是这些索引的键不是唯一的。每个关键字可以有多条相关记录。
结论
提供了关于数据库如何存储和检索数据的见解。对于存储大量数据用于分析的数据仓库,上述所有方法可能都不是最佳的。为下一篇文章保留数据仓库的存储和检索过程!
要阅读更多关于数据库和应用程序设计的内容,我强烈推荐阅读马丁·克莱曼的《设计数据密集型应用程序》。
希望你喜欢这篇文章!
数据科学中的实验设计
约翰·霍普斯金 DS 专业化系列
数据科学中的实验需要适当的规划和设计来最好地回答它的问题
由 Unsplash 上 Greg Rakozy 拍摄的照片
[Full series](https://towardsdatascience.com/tagged/ds-toolbox)[**Part 1**](/the-data-scientists-toolbox-part-1-c214adcc859f) - What is Data Science, Big data and the Data Science process[**Part 2**](/how-to-learn-r-for-data-science-3a7c8326f969) - The origin of R, why use R, R vs Python and resources to learn[**Part 3**](/a-crash-course-on-version-control-and-git-github-5d04e7933070) - Version Control, Git & GitHub and best practices for sharing code.[**Part 4**](/the-six-types-of-data-analysis-75517ba7ea61) - The 6 types of Data Analysis[**Part 5**](/designing-experiments-in-data-science-23360d2ddf84) - The ability to design experiments to answer your Ds questions[**Part 6**](/what-is-a-p-value-2cd0b1898e6f) - P-value & P-hacking [**Part 7**](/big-data-its-benefits-challenges-and-future-6fddd69ab927) - Big Data, it's benefits, challenges, and future
本系列基于约翰·霍普斯金大学在 Coursera 上提供的 数据科学专业 。本系列中的文章是基于课程的笔记,以及出于我自己学习目的的额外研究和主题。第一门课, 数据科学家工具箱 ,笔记会分成 7 个部分。关于这个系列的注释还可以在这里找到。
介绍
在解决问题之前问正确的问题是一个很好的开始,它在复杂中为你指明了正确的方向。但是要真正有效地解决你的问题,你还需要适当的规划和良好的实验设计。类似于用科学方法进行实验,这需要遵循一系列简洁的步骤来进行安全实验,即设备的名称和某些东西的测量,您还必须设计您的数据科学实验。只有这样,才能铺出一条清晰的道路,用正确的方法一部分一部分地、一丝不苟地解决你的问题。以下是更多关于实验设计的内容。
什么是实验设计?
这通常意味着组织一个实验以便你有正确的数据到有效地回答你的 DS 问题。
在开始解决 DS 问题之前,可能会出现一些问题。
- 回答这个问题的最好方法是什么?
- 你在衡量什么,如何衡量?
- 什么是正确的数据?
- 我如何收集这些数据?
- 我使用什么工具和库?
- 等等…
所有这些问题都是 DS 实验的关键部分,必须从一开始就回答。如果没有适当的规划和设计,您将在整个工作流程中面临许多这样的问题,从而使工作效率低下。
那么在一个实验中什么是正确的流程呢?
实验设计流程
1.提出问题
2.设计实验
3.识别问题和错误来源
4.收集数据
这个过程开始于在任何数据收集之前明确地制定你的问题,然后设计尽可能好的设置来收集数据以回答你的问题,识别的问题或你设计中的错误来源,然后收集数据。
重要
坏数据→错误分析→错误结论
- 错误的结论可能会产生涓滴效应(论文中的引用最终会应用于真实的医学案例)
- 对于高风险的研究,如确定癌症患者的治疗计划,实验设计至关重要。
- 数据不好、结论错误的论文会被撤稿,名声不好。
实验设计原则
1.独立变量(x 轴)
- 被操纵的变量,不依赖于其它变量
2.因变量(y 轴)
- 预期因自变量变化而变化的变量
3.假设
- 对变量和实验结果之间关系的有根据的猜测
例子
这里有一个实验设计的例子,用来测试阅读书籍和读写能力之间的关系。
- 假设:随着书籍阅读量的增加,读写能力也会提高
- X 轴:阅读的书籍
- Y 轴:识字
- 实验设置:我假设文化水平取决于阅读的书籍
- 实验设计— 测量 100 个人的书籍阅读和读写能力
- 样本量(n) — 实验中包含的实验对象数量
在收集数据来测试你的假设之前,你首先要考虑可能导致你的结果出错的问题,其中之一就是混杂因素。
什么是混杂因素?
- 可能影响因变量和自变量之间关系的外来变量
对于这个例子,混杂因素可以是年龄变量,因为它既可以影响阅读的书籍数量,也可以影响识字率。读的书和识字的任何关系都可能是年龄造成的。
假设:读书→识字
混杂因素:读过的书←年龄→识字
记住混杂因素,设计控制这些混杂因素的实验是至关重要的,这样它就不会影响你的结果,并且你正在正确地设计你的实验。
有三种方法可以处理混杂因素:
- 控制
- 随机化
- 复制
1.控制
回到读书识字实验,为了控制年龄对结果的影响,我们可以测量每个人的年龄以考虑年龄对识字的影响。
这将在您的研究中创建两个组:
(1) 对照组 & (2) 治疗组,其中:
- 对照组→固定年龄的参与者
- 治疗组→各年龄段的参与者。
- 比较结果,了解固定年龄组与不同年龄组的识字能力是否不同。
另一个常见的例子是药物测试,为了测试药物对患者的效果,对照组不接受药物,而治疗组接受药物,并比较效果。
安慰剂效应
这是一种偏见,一个人在心理上认为某种治疗对他们有积极影响,即使根本没有治疗。这在医学上很常见,安慰剂药物可以替代真正药物的作用。
为了解决这种偏见,受试者将被蒙住双眼,这意味着他们不知道自己被分到哪一组:
- 对照组与模拟治疗(糖丸,但告诉药物)
- 治疗组采用实际治疗
- 如果存在安慰剂,两组患者的感受是一样的
2.随机选择
随机化基本上是将个体随机分配到不同的组,这是很好的,有两个原因,(1)你不知道混杂变量,和(2)它减少了偏向一个组以丰富混杂变量的风险
以药物测试为例
- 对许多受试者取样,进行随机化
- 将参与者分配到两个主要组,一个混杂组,另一个随机,每个组都有自己的对照组和治疗组
- 从随机分组中,你可以看出是否存在偏见。
3.分身术
复制基本上是重复你的实验,但这次是用不同的对象,这很重要,因为它显示了你的实验的可重复性。
复制也是必要的,主要是因为只进行一个实验可能是偶然的,这是许多因素的结果,例如:
- 混杂因素分布不均匀
- 数据收集中的系统误差
- 极端值
如果复制完成了(用一组新的数据)并且产生了相同的结论,这表明该实验是强有力的,并且具有良好的设计。
此外,复制的核心是数据的可变性,这与 P 值有关,P 值有一个黄金法则( P ≤ 0.05 ),这是许多人在统计假设检验中努力追求的。在下一篇文章中会有更多的介绍
摘要
建设一个像纽约这样的城市需要蓝图和非常精确的规划,这创造了你今天所看到的。在数据科学中设计实验也应该如此。
这是实验设计的基础,从根本上讲是关于精确的计划和设计,以确保您的分析或研究有适当的数据和设计,从而防止错误的结论。
流程的一个很酷的助记符是QDPC-Qu 问题DATAPLANCare fully
制定问题→设计实验→识别问题和错误→收集数据。
错误的结论会产生涓滴效应,因为它们在现实生活中被引用和使用,特别是在医学上。
原理:
- 独立变量 (x 轴)——被操纵且不受其他变量影响
- 因变量 (y 轴)—x 的变化导致 y 的变化
- 假设 —关于 X 和 Y 之间关系的有根据的猜测
实验设计
- 混杂因素——影响自变量和因变量之间关系的外来变量
- 控制——在实验中固定混杂因素或考虑和测量
- 对照组——一组测量了独立变量但未接受治疗的受试者。作为实验的对照,比较变化和效果
- 盲法受试者 —将受试者盲法分组,以测试安慰剂效应
- 安慰剂效应 —一个人对安慰剂治疗产生的效应的信念
- 随机化 — 随机分配参与者,以呈现偏倚和混杂效应
- 复制— 重复实验到强化结果的结论
参考
如果您对学习数据科学感兴趣,请查看“超学习”数据科学系列!
这是一个简短的指南,基于《超学习》一书,应用于数据科学
medium.com](https://medium.com/better-programming/how-to-ultralearn-data-science-part-1-92e143b7257b)
查看这些关于数据科学资源的文章。
[## 2020 年你应该订阅的 25 大数据科学 YouTube 频道
以下是你应该关注的学习编程、机器学习和人工智能、数学和数据的最佳 YouTubers
towardsdatascience.com](/top-20-youtube-channels-for-data-science-in-2020-2ef4fb0d3d5) [## 互联网上 20 大免费数据科学、ML 和 AI MOOCs
以下是关于数据科学、机器学习、深度学习和人工智能的最佳在线课程列表
towardsdatascience.com](/top-20-free-data-science-ml-and-ai-moocs-on-the-internet-4036bd0aac12) [## 机器学习和数据科学的 20 大网站
这里是我列出的最好的 ML 和数据科学网站,可以提供有价值的资源和新闻。
medium.com](https://medium.com/swlh/top-20-websites-for-machine-learning-and-data-science-d0b113130068) [## 开始数据科学之旅的最佳书籍
这是你从头开始学习数据科学应该读的书。
towardsdatascience.com](/the-best-book-to-start-your-data-science-journey-f457b0994160)
联系人
如果你想了解我的最新文章,请通过媒体关注我。
也关注我的其他社交资料!
请关注我的下一篇文章,记得注意安全!*
为数字双胞胎设计
法耶·康尼什在 Unsplash 上的照片
数字双胞胎的未来是多样化的——在意义和设置上都是如此。忽视这一现实将阻碍数字双胞胎的成功。
这里,重点是在分散的服务和数据环境中成功实施数字双胞胎的设置和设计考虑因素(因此这里不包括物联网摄取和数据模型部分)。呈现了初始技术映射。
数码双胞胎
什么被视为数字孪生取决于它的目标。一个例子是现实生活中的数字孪生资产,比如一个车队。基于车队的状态,将实时做出关于路线、驾驶员刹车等的决策。这些系统还将依赖于外部数据,如交通信息和环境条件,如天气。由于决策将逐渐基于先进的算法,历史事件的模式将变得重要,并将逐渐包括更加多样和丰富的数据。数字版本变得越来越丰富,足以建立对重要角色互动的理解。有了这种理解,就可以构建模拟,并且可以根据模拟运行的结果通过选择过程做出自动决策。换句话说,一个人正在构建一个数字真相,以及围绕它所关注的资产的平行未来可能性。
怀着另一个目标,另一种类型的数字孪生将被建立。例如,如果一个人在不断变化的气候中寻求缓解措施,那么数据、算法、模拟、考虑的时间间隔会有很大的不同。导致数字双胞胎之间的决策或建议不同——即使它们部分涉及相同的对象。数字双胞胎的决策取决于他们的 KPI(关键绩效指标)是否符合他们的目标和方法。数字双胞胎将有助于优化运营,但也将有助于解决社会问题,尽管潜在的社会、经济、道德、责任和环境考虑因素应在更广泛的受众中平行进行,因为这将影响数字双胞胎设计的 KPI。
分散风景
数字双胞胎有三个共同的组成部分——数据、计算、算法——但每个组成部分的用途及其驻留位置在数字双胞胎之间会有很大的不同。很可能来自自有资产的数据将直接流向自有的受控计算环境(本地、公共或私有云中)。然而,在其他情况下,通过本地超级计算机生成的大量气候数据甚至可以在磁带上“锁定”多年,在这种情况下,获取和下载数据将成为一项艰巨的任务。由于没有任何组织能够或愿意将所有数据集中在一个地方,数字双胞胎的未来是一个分散的景观,原始数据驻留在许多不同的位置。类似地,算法和应用程序将被创建,并由不同的组织提供,并在有意义的地方运行,因此通常接近数据。换句话说,数字双胞胎的未来将在很大程度上基于松散耦合的类似网格计算的环境。
适应网格计算
数字双胞胎环境中的网格计算使得数据提取、数据转换和算法应用以复杂和地理上分散的方式链接起来。本质上,网格计算的用户对整个链的控制程度较低,网格计算的成功,以及许多数字双胞胎的成功,将依赖于这些链服务的可访问性、可靠性和透明性。因此,为数字双胞胎设计服务的使用和吸收需要考虑几个网格计算问题。设计原则需要在数字双胞胎的两个层面上考虑,即’云’*、和’*云间的交互。**
云内交互
内部云是数字孪生提供商运营数字孪生服务或应用的云环境,它只是提供商选择的位置,因此可以位于公共云中,也可以位于私有云或内部环境中。
数字孪生可以纯粹基于提供商在这个(内部)云环境中收集和汇集的数据和处理能力。要让这对双胞胎正常工作,需要遵循几个原则。从存储到计算内存的数据传输应仅限于所需的数据。因此,数据应该以云优化的方式进行格式化、分区和索引。只有在主动请求时,数据才会被传输到计算内存。因此,实现了遵循懒惰计算原则的高效数据传输。数字孪生应用请求数据或激活处理服务,这些数据或服务可通过访问层(可能通过 API)获得。
在数字孪生提供商运行其数字孪生的相同云环境中,可以存在其他服务提供商,它们为数字孪生提供数据和有价值的数据服务。最常见的是云提供商本身,但也可能是第三方。关于可访问性和数据传输的相同原则可以适用于这些方提供的服务。云提供商可以将数据集直接“安装”到 Digital Twin 提供商的计算引擎,而无需复制活动。随着链接来自多个提供商的服务变得越来越复杂,出现了对控制链的数字孪生编排者的需求。
一个云环境中的数字孪生设置:云内交互(图片由作者提供)
云间交互
如上所述,数据所有者将使他们的数据可从各种位置和环境(本地、私有云、一个或多个公共云)访问,而许多数据集太大,无法及时传输到另一个环境。数字双胞胎必须应对跨云交互。因为需要将数据从一个云传输到另一个云,所以保持数据集尽可能小就更加重要了。因此,建议数据所有者使用一组丰富的 API,遵循云内交互中描述的关于数据访问、提取和传输效率的相同原则。如果数据所有者不遵循这些原则,Digital Twin 所有者应考虑在其他云环境中运行他们的部分计算,然后将最少的数据传输到他们自己的环境中(例如通过数据聚合或 ML 推理来整合数据)。为了降低这里的复杂性,Digital Twin 所有者可以选择对多个云环境应用相同的数据存储、计算引擎和访问层设置。从开发人员和研究人员的角度来看,如果可以建立一个开发经验来建立基础设施,以及将智能构建到计算引擎中,他们的生产力将会提高。
与云内交互相比,更需要一种基于服务的工作流方法来克服分布式环境特有的挑战。数据传输、工作流控制和异常处理的复杂性使得很难定义、协调和保证流程。Digital Twin orchestrator 应该适应这些工作流程协调。
多云环境中的数字孪生设置:云间相互作用(图片由作者提供)
初始技术映射*
考虑到最终用户,数字双胞胎可以用于多种责任和角色。数字孪生应用可以针对他们的特定需求进行设计,从面向决策者的仪表盘(包括地图)到面向资产运营商或政策制定者的增强现实等沉浸式体验。用于分布式环境的基于服务的编排器需要进一步关注潜在技术的映射。服务访问层的一项常见技术是使用 API。大规模分析大多以批处理方式运行,不一定是实时的,这使得分布式和并行计算引擎成为数字双胞胎的候选,这些引擎可以是基于 Spark 的(分布式)或基于 Dask /python 的(并行)——这两种方法都得到大型动态开源社区的支持,这些社区在数据科学、机器学习和其他算法方法方面进展迅速。从地球/海洋/气候研究人员开始, Pangeo 项目就是一个基于 Dask 的例子。研究人员和开发人员的开发体验可以基于笔记本技术。实时处理需要通过流技术来实现,数据接收和存储应支持快速写入。为 Digital Twin 应用程序准备的数据需要符合该应用程序和用户体验的需求,如 AR 应用程序的快速查询和服务功能。数据提供者,无论是作为数字双胞胎的一部分还是作为数据所有者,都应该促进快速的数据提取和交付,可以通过使用云原生格式(如地理空间数据的 COG、Zarr)。
结论
数字双胞胎的未来是分散的:数据将可以从许多不同的环境中以许多不同的数据格式和访问方法获得。为了将所有这些整合在一起,并得出可行的见解,数字双胞胎需要针对这一现实进行设计。需要关注云间和云内环境交互的内在复杂性,并尽可能努力降低成功使用数字双胞胎的复杂性。
*** Azure 平台和 Planetary Computer 的技术映射可在此处阅读:与 Dask 和 Pangeo 一起走向 Azure 中的环境数字双胞胎| Remko de Lange 著| 2021 年 4 月|走向数据科学**
这里的意见是我的。
设计智能 Python 词典
设计智能、用户友好的 Python 词典的快速指南
照片由 Aaron Burden 在 Unsplash 上拍摄
上周在做一个业余爱好项目时,我遇到了一个非常有趣的设计问题:
你如何处理错误的用户输入?
让我解释一下。
Python 字典示例。来源:作者
Python 中的 字典 表示成对的键和值。例如:
student_grades = {'John': 'A', 'Mary': 'C', 'Rob': 'B'}# To check grade of John, we call
print(student_grades['John'])
# Output: A
当您试图访问一个不存在的键时会发生什么?
print(student_grades['Maple'])
# Output:
KeyError Traceback (most recent call last)
<ipython-input-6-51fec14f477a> in <module>
----> print(student_grades['Maple'])
KeyError: 'Maple'
你收到一个 键错误 。
每当
dict()
对象被请求字典中不存在的key
的值时,就会发生 KeyError。
当您接受用户输入时,这个错误变得极其常见。例如:
student_name = input("Please enter student name: ")
print(student_grades[student_name])
本教程提供了几种方法来处理 Python 字典中的关键错误。
我们将努力构建一个智能 python 字典,它可以处理用户输入中的各种错别字。
来源: Imgur
设置默认值
一个非常懒惰的方法是每当请求的键不存在时返回一个默认值。这可以使用get()
方法来完成:
default_grade = 'Not Available'
print(student_grades.get('Maple',default_grade))# Output:
# Not Available
你可以在这里阅读更多关于get()
方法的内容。
处理信件案件
让我们假设您有一本包含特定国家人口数据的字典。该代码将要求用户输入一个国家名称,并打印其人口。
# Output
Please enter Country Name: France
65
但是,让我们假设用户键入输入为‘france’
。*目前,在我们的字典中,所有键的首字母都是大写的。*输出会是什么?
Please enter Country Name: france-----------------------------------------------------------------KeyError Traceback (most recent call last)
<ipython-input-6-51fec14f477a> in <module>
2 Country_Name = input('Please enter Country Name: ')
3
----> 4 print(population_dict[Country_Name])
KeyError: 'france'
由于‘france’
不是字典中的一个键,我们收到一个错误。
一个简单的变通方法:用小写字母存储所有国家的名称。
此外,将用户输入的任何内容转换成小写。
Please enter Country Name: france
65
处理错别字
但是,现在假设用户输入的是‘Frrance’
而不是‘France’
。我们该如何处理这件事?
来源:吉菲
一种方法是使用 条件语句 。
我们检查给定的 user_input 是否可以作为键使用。如果它不可用,那么我们打印一条消息。
最好将它放在一个循环中,并在一个特殊的标志输入(如 exit)上中断。
循环将继续运行,直到用户输入exit
。
输出。来源:作者
更好的方法
虽然上述方法**‘有效’,但它不是我们在介绍中承诺的‘智能方法’**。
来源: Giphy
我们希望我们的程序是健壮的,并检测简单的错别字,如frrance
和chhina
(非常类似于谷歌搜索)。
经过一番研究,我找到了几个符合我们需求的库。我最喜欢的是标准 python 库: difflib 。
difflib 可用于比较文件、字符串、列表等,并产生各种格式的差异信息。
该模块提供了各种用于比较序列的类和函数。
我们将使用 difflib 中的两个特性:sequence matcher和get _ close _ matches。
让我们简单看一下他们两个。如果您只是对应用程序感到好奇,可以跳到下一节。
#序列匹配器
**SequenceMatcher 类用于比较两个序列。**我们将其对象定义如下:
difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)
isjunk
:用于指定junk
元素(空格、换行符等)。)是我们在比较两个文本块时希望忽略的。我们在这里通过None
。a
和b
:我们希望比较的字符串。autojunk
:自动将某些序列项目视为垃圾的启发式。
让我们用 SequenceMatcher 来比较两个字符串chinna
和china
:
在上面的代码中,我们使用了ratio()
方法。
ratio 返回序列相似性的度量,作为范围[0,1] 中的浮点数。
里卡多·戈麦斯·安吉尔在 Unsplash 上拍摄的照片
#获取 _ 关闭 _ 匹配
现在,我们有了一种基于相似度比较两个字符串的方法。
但是,如果我们希望找到与特定字符串相似的所有字符串(存储在数据库中),会发生什么呢?
get_close_matches()
从可能性列表中返回包含最佳匹配的列表。
difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)
word
:需要匹配的字符串。possibilities
:与word
匹配的字符串列表。- 可选
n
:返回的最大相近匹配数。默认情况下,3;必须大于 0 。 - 可选
cutoff
:相似率必须大于该值。默认情况下, 0.6 。
可能性中的最佳 n 匹配在一个列表中返回,按相似性得分排序,最相似的排在最前面。
让我们来看一个例子:
把所有的放在一起
现在我们已经有了 difflib,让我们把所有的东西放在一起,构建一个防输入错误的 python 字典。
我们必须关注用户给出的Country_name
在population_dict.keys()
中不存在的情况。在这种情况下,我们尝试找到一个与用户输入名称相似的国家,并输出其人口。
# pass country_name in word and dict keys in possibilities
maybe_country = get_close_matches(Country_Name, population_dict.keys())# Then we pick the first(most similar) string from the returned list
print(population_dict[maybe_country[0]])
最终的代码将需要考虑一些其他情况。例如,如果没有相似的字符串,或者用户确认这是否是他们需要的字符串。看一看:
输出:
Inida 被理解为印度。来源:作者
结论
本教程的目标是为您提供一个构建对用户输入健壮的字典的指南。
我们研究了处理各种错误的方法,比如大小写错误和小的错别字。
我们可以在此基础上进一步研究各种其他应用。示例:使用 NLPs 更好地理解用户输入,并在搜索引擎中带来附近的结果。
希望你觉得这个教程有用!
重要链接
- Python 词典介绍
- difflib —计算增量的助手
为初创公司设计 ML 流程编排系统
办公时间
构建轻量级生产级 ML 编排系统的案例研究
我最近有机会在一家医疗保健初创公司建立了一个机器学习平台。
本文涵盖了架构设计之旅、技术权衡、实现细节和经验教训,作为关于为初创公司设计机器学习编排平台的案例研究。
随着机器学习工具生态系统的不断成熟和扩展,为生产数据科学管道构建机器学习编排层的选项并不短缺。更相关的挑战是能够确定哪些工具适合您组织的需求案例。
宝贵的经验是使用数据科学家已经熟悉的工具来构建源代码控制过程。
我们最终得到的工具集是:
- 版本控制的 SQL 脚本支持源数据集提取
- 预处理由 Docker 容器中执行的 Python runner 脚本抽象的代码,访问数据湖中的静态检查点数据。
- 在一个 Python 模型类中抽象的模型训练代码,该模型类自包含用于加载数据、工件序列化/反序列化、训练代码和预测逻辑的函数。
- 样板瓶 API 端点包装器,用于执行健康检查并返回推理请求。
端到端的构建和转换模型管道。[来源:图片由作者提供]
以下部分涵盖了最终产品所需的思考过程和实现细节。具体的系统架构可能无法推广到所有的组织,但这个案例研究可以提供一个有用的思考练习,告诉我们如何通过选择正确的工具和比较架构权衡。
构建该平台的设计流程始于建立正确的引导性问题,引导架构讨论。这有助于在技术构建过程中形成有利于权衡的框架。
指导性问题
- 服务的目标用户是谁?他们目前使用什么技术技能,他们愿意学习什么技能?
- 用户需要什么样的数据访问模式,会产生什么类型的推论?数据 ETL 是如何在组织内部发生的?
- 数据流的新鲜度要求是什么?
- 哪些组件需要监控?哪些指标有用?
- 模型版本多久升级一次?
在提出一个架构决策记录(ADR)提案后,该提案列出了问题范围和初始架构,团队和我就如何实现 POC 达成了一个粗略的计划。
关键决策
- 我们需要一个源代码控制过程来规范模型部署模式。
- Docker 图像应作为便携性、再现性和缩放模型解决方案的常见选择。
- AWS Sagemaker 将成为虚拟化资源管理的编排支柱,用于部署 API、管理批量转换作业、身份验证、管道可观察性和负载平衡推理流量。
- Airflow 将充当任务编排系统,而 Sagemaker 负责资源分配——调度和执行是分离的。
- 我们需要一个共享状态控制器应用程序来协调和管理 Sagemaker 操作。
- **SQLAlchemy ORMs 可以构建数据模型,结合了用于资源状态转换的有限状态机,**清晰地分离了机器学习模型生命周期的步骤,并限制了重复的 Sagemaker 操作。
- 对于用户来说,管道的配置和执行应该尽可能接近一键部署。
确定了关于工具和关键特性的更高层次的问题后,我花了几个月的时间在这个框架上提炼一个最小可行的产品。这是我第一次从零开始构建机器学习平台,因此出现了许多意想不到的实现挑战,需要在整个编码过程中做出一些决策。
机器学习编排概念验证系统的架构设计。[来源:图片由作者提供]
“完成”是什么样子的?
- 保持第一个模型简单,并获得正确的基础设施。
- 一个好的选择是以“中立的第一次启动”为目标,明确降低机器学习收益的优先级。
第一步是确定我们如何知道我们什么时候完成了。为了奠定最初的基础设施和集成测试,我们设定了一个目标,即部署一个中立的首次发布,而不考虑机器学习模型的性能。我们的想法是保持第一个模型简单,并获得正确的基础设施。这个想法的灵感来自于谷歌将有效的机器学习框架形式化的规则。
最初部署的几个模型可能是高价值、易实现的成果,不需要复杂和花哨的功能来进行培训和部署。
重点应该是:
- 数据存储和计算是如何协调的?
- 确定系统的成功和失败状态。
- 将模型集成到您的框架中——如何触发离线批处理计算,以及如何部署和关闭实时 API 端点?
目标是从简单的功能开始:
- 确认训练数据被正确地接收、转换并适合模型。
- 模型重量分布看起来很合理。
- 特征数据可以正确访问离线和在线模型进行推理。
测试
- 测试基础设施独立于机器学习。
- 在 CI/CD 中对机器学习模型进行单元测试既困难又浪费精力— 使用测试数据夹具为 docker 镜像编写集成测试。
- 在导出模型之前检测问题*。*
确保基础设施具有单元测试和集成测试,并且系统的学习部分被封装以允许 fixtures 和 stubs 模拟运行时条件。
具体来说:
- 测试将数据输入算法。理想情况下,通过定义数据质量的可接受性阈值来检查缺失的要素列。
- 如果隐私政策允许,对训练算法的输入进行手动检查可能是一个很好的健全检查。
- 关于训练数据和服务数据分布的统计数据是对抗概念漂移的一个有价值的监视器。
- 测试算法产生的模型工件。经过训练的模型应该是可再现的,并且在相同的数据上与服务模型具有同等的性能。
配置
- 配置应尽可能集中在上游*。采购配置的选项可以通过数据库表配置,也可以通过受源代码控制的 YAML 文件。*
- 配置灵活性应根据用户变更的预期频率以及管道稳定性需求来确定。配置可以在源代码控制之外更改吗?一个 ML 服务能容忍一个坏的配置造成的中断吗?
启用和管理机器学习计划是一项概率性工作。很难预测哪些问题容易或难以解决,以及哪些方法和模型配置将产生最佳模型。开发一个好的模型经常需要许多次优的迭代,开发的速度是非线性的。由于不同的背景、开发风格和价值观,机器学习和工程团队之间可能存在文化差距。
这些项目成功的一个关键衡量标准是减少对部署到产品中的模型数量的关注,而更多地关注测试模型被执行的迭代次数。在机器学习产品领域,实现快速、轻量级的实验比构建复杂、功能繁多的研发模型更有价值。
因此,简单、轻量级 ML 模型的模型可移植性和易于配置是比投资复杂、高强度的 R&D 算法支持(如 Tensorflow、Pytorch 或 Spark 框架)更有价值的初始目标。
任务编排
- Dag 工作流应该在工作流开始时实例化一次运行时参数,并将参数推送到任务。
- 存储成本低廉 —在数据提取、预处理和训练过程中,只要有可能,检查点和持久化数据计算会更安全。
- 确保你像发球一样训练的最好方法是保存发球时使用的功能集,然后将这些功能传送到日志中,以便在训练时使用。
API 端点
- **共享状态控制器应用程序跨部署正确地对 Sagemaker Sagemaker 型号进行版本控制,以支持蓝绿色部署和金丝雀型号。
- **不同 Sagemaker API 有效负载字典的注册表为操作调用定义默认参数,在运行时调用时注入定制配置。
- **S3 是我们选择的工件存储框架。我们需要一组实用函数来标准化跨推理、训练、序列化和墓碑文件的 S3·URI 命名空间的构造。
将模型部署为用于实时推理的 API web 服务的关键步骤:
- ***调用模型:*为模型编写一个轻量级 API 来处理推理请求,并返回预测、输入数据、模型元数据属性,以及附加唯一的事务 id 来构建审计跟踪。
- ***虚拟化:*用适当网络覆盖、认证协议和应用程序路由将 API 对接并部署到集群。
- ***水平可伸缩性:*配置自动伸缩、负载平衡、日志、认证协议和任何其他需要的基础设施。
除了简单地建立一个 ML API 端点所需的工作之外,还需要考虑如何在构建新模型时提高开发人员的工作效率。
- API 推理访问模式在部署的模型中是相当统一的吗?也许这些轻量级端点中的大多数都可以通过样板代码抽象出来。
- 什么类型的推理请求最常见?如果吞吐量比延迟更有价值,那么用输入文件构建批量预测功能可能比启用实时 API 点推断更优先。
- 用于培训和服务的数据 ETL 是主要来自几个不同的数据存储库,还是有一组高度多样化的管道输入?这些输入多久改变一次?自助式数据 ETL 可以由数据库表配置快速驱动,或者通过在源代码控制存储库中配置 YAML 文件来稳定,但灵活性较低。
监控
- **你们型号的性能退化率是多少?一天中准确性指标损失的百分比是多少?一个月?四分之一?通过计算这一退化率,您可以确定模型的重新训练新鲜度要求的优先级。
- **在持久化工件和部署到生产之前投资测试。鉴于 CI/CD 文化和通过测试而不是监控来捕捉错误的最佳实践,这似乎是不言而喻的,但机器学习模型除了对推理管道上的数据流进行集成测试之外,还需要对模型权重分布和推理准确性进行回归测试。
有三个典型的问题会降低已部署的 ML 模型的性能。
- 概念漂移:随着时间的推移,由于训练数据和真实世界服务数据之间的数据分布漂移越来越大,生产模型的准确性会降低。现有的客户偏好可能会改变。
- 地区:在给定背景(即地理、人口统计、行业)下训练的模型在外推至新群体时表现更差。随着新用户加入模型,新客户可能会对数据集引入新的偏好。
- 数据质量 : ML 模型对无声数据故障特别敏感,例如,陈旧的引用表、事件流中特定列的中断、上游数据集的新词汇或模式,或者进入数据集的畸形数据。
一路走来的经验教训
- 仅在适当的时候添加流程 —机器学习编排框架的成功不是你成功地将多少模型部署到生产中,而是启用的实验的数量。
- 不依赖于框架。围绕工具的决策过程应由 ML 服务需求、数据科学产品用户的规模和专业知识、云供应商和行业隐私政策等组织因素以及内部数据平台的成熟度来驱动。
- 向抽象方向构建。数据科学管道中可以抽象出来的部分越多,测试每个组件和从检查点重启失败的管道就越容易。
- 瞄准中立的首发。这消除了展示已部署的 ML 模型的直接价值的压力,并允许将空间集中在获得基础设施、许可和胶水上,以使架构可操作。
在我探索 ML 资源和框架的过程中,我遇到了这些惊人的博客、文章和演示文稿,它们帮助指导了我的旅程。我把它们列在这里,是为了方便那些希望更深入地编写和建模机器学习管道的人:
欢迎在 LinkedIn 、 Twitter 、 Github 或 Medium 上与我联系!
基于预训练模型集成的破坏性图像分类
通过在 Tensorflow 中制作预训练网络(如 InceptionV3、MobileNetV2 和 Xception)的集成堆叠集成模型,消除图像分类任务
预先训练的网络非常酷。它们提供了很高的精确度,并且不需要花很多时间来训练。那么有什么能比预先训练好的网络更好呢?用其中的两个。最好用三个。或者事实上,使用多少你想要在一起作为一个系综模型和破坏图像分类任务。
要求
如果你想编码,你需要 Tensorflow 和 OpenCV。你也可以像我一样使用 Google Colab,它会预装我们任务所需的所有软件包,还提供免费的 GPU。
加载数据集
被选择消灭的数据集是经典的猫对狗数据集。由于它是一个小数据集,我们将把它完全加载到内存中,以便它训练得更快。
import tensorflow as tf
import os
import numpy as np
import matplotlib.pyplot as plt
import re
import random
import cv2_URL = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'
path_to_zip = tf.keras.utils.get_file('cats_and_dogs.zip', origin=_URL, extract=True)
PATH = os.path.join(os.path.dirname(path_to_zip), 'cats_and_dogs_filtered')train_dir = os.path.join(PATH, 'train')
validation_dir = os.path.join(PATH, 'validation')train_cats_dir = os.path.join(train_dir, 'cats')
train_dogs_dir = os.path.join(train_dir, 'dogs')
validation_cats_dir = os.path.join(validation_dir, 'cats')
validation_dogs_dir = os.path.join(validation_dir, 'dogs')cats_tr = os.listdir(train_cats_dir)
dogs_tr = os.listdir(train_dogs_dir)
cats_val = os.listdir(validation_cats_dir)
dogs_val = os.listdir(validation_dogs_dir)cats_tr = [os.path.join(train_cats_dir, x) for x in cats_tr]
dogs_tr = [os.path.join(train_dogs_dir, x) for x in dogs_tr]
cats_val = [os.path.join(validation_cats_dir, x) for x in cats_val]
dogs_val = [os.path.join(validation_dogs_dir, x) for x in dogs_val]total_train = cats_tr + dogs_tr
total_val = cats_val + dogs_val
所有训练和验证(在本例中为测试)图像的路径都存储在 total_train 和 total_val 中。我们将使用 OpenCV 来读取图像,并将它们存储在具有维度(图像数量 x 图像形状 x 通道)的 NumPy 数组中。它们对应的标签也将存储在一维 NumPy 数组中。
def data_to_array(total):
random.shuffle(total)
X = np.zeros((len(total_train), 224, 224, 3)).astype('float')
y = []
for i, img_path in enumerate(total):
img = cv2.imread(img_path)
img = cv2.resize(img, (224, 224))
X[i] = img
if len(re.findall('dog', img_path)) == 3:
y.append(0)
else:
y.append(1)
y = np.array(y)
return X, yX_train, y_train = data_to_array(total_train)
X_test, y_test = data_to_array(total_val)
创建集合模型
要遵循的步骤
训练单个模型并保存它们
我们的第一个任务是创建所有单独的模型。我将使用 MobileNetV2、InceptionV3 和 Xception 创建三个不同的模型。在 Tensorflow 中,使用预先训练的网络创建模型非常容易。我们需要加载权重,决定是冻结还是解冻加载的权重,最后添加密集层,使输出达到我们想要的效果。我将为我的模型使用的基本结构:
def create_model(base_model):
base_model.trainable = True
global_average_layer = tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
prediction_layer = tf.keras.layers.Dense(1, activation='sigmoid')(global_average_layer)
model = tf.keras.models.Model(inputs=base_model.input, outputs=prediction_layer)
model.compile(optimizer=tf.keras.optimizers.Adam(lr=0.0001), loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), metrics=["accuracy"])
return model
在创建我们的模型之后,我们需要将它们与我们的训练数据进行一些时期的拟合。
batch_size = 32
epochs = 20def fit_model(model):
history = model.fit(X_train, y_train,
batch_size=batch_size,
steps_per_epoch=len(total_train)//batch_size,
epochs=epochs,
validation_data=(X_test, y_test),
validation_steps=len(total_val)//batch_size)
return historyIMG_SHAPE = (224, 224, 3)
base_model1 = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE, include_top=False, weights="imagenet")
base_model2 = tf.keras.applications.InceptionV3(input_shape=IMG_SHAPE, include_top=False, weights="imagenet")
base_model3 = tf.keras.applications.Xception(input_shape=IMG_SHAPE, include_top=False, weights="imagenet")model1 = create_model(base_model1)
model2 = create_model(base_model2)
model3 = create_model(base_model3)history1 = fit_model(model1)
model1.save('models/model1.h5')history2 = fit_model(model2)
model2.save('models/model2.h5')history3 = fit_model(model3)
model3.save('models/model3.h5')
让我们看看我们的模特们自己的表现。
MobileNetV2 的结果
InceptionV3 的结果
例外的结果
结果一点也不差,但我们仍将改进它们。
加载模型并冻结其图层
我们的下一步是加载我们刚刚在上面创建的模型,并冻结它们的层,这样当我们在它们上面安装我们的集合模型时,它们的权重不会改变。
def load_all_models():
all_models = []
model_names = ['model1.h5', 'model2.h5', 'model3.h5']
for model_name in model_names:
filename = os.path.join('models', model_name)
model = tf.keras.models.load_model(filename)
all_models.append(model)
print('loaded:', filename)
return all_modelsmodels = load_all_models()
for i, model in enumerate(models):
for layer in model.layers:
layer.trainable = False
连接它们的输出并添加密集层
将所有模型的输出放入一个连接层。然后添加具有一些单元的密集层,接着是具有单个输出和激活等于“sigmoid”的密集层,因为我们的任务是二进制分类。这可以被认为是一个人工神经网络,其中所有模型的预测作为输入,并提供一个输出。
ensemble_visible = [model.input for model in models]
ensemble_outputs = [model.output for model in models]
merge = tf.keras.layers.concatenate(ensemble_outputs)
merge = tf.keras.layers.Dense(10, activation='relu')(merge)
output = tf.keras.layers.Dense(1, activation='sigmoid')(merge)
model = tf.keras.models.Model(inputs=ensemble_visible, outputs=output)
编译和训练集合模型
我使用了经典的“Adam”优化器,学习率略高,为 10x-3,来编译模型。
model.compile(optimizer=tf.keras.optimizers.Adam(lr=0.001), loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), metrics=["accuracy"])
让我们看看我们的模型现在是什么样子。
集合模型
我们能像训练我们的个体模型一样,通过传递数据集来训练它吗?不要!在三个地方需要输入,而只产生一个输出。所以我们需要像这样配置我们的 X 值。
X_train = [X_train for _ in range(len(model.input))]
X_test = [X_test for _ in range(len(model.input))]
现在我们可以像以前一样拟合模型了。
history = model.fit(X, y_train,
batch_size=batch_size,
steps_per_epoch=len(total_train) // batch_size,
epochs=epochs,
validation_data=(X_1, y_test),
validation_steps=len(total_val) // batch_size)
结果
首先,让我们为系综模型绘制图表。
集合模型的结果
我只对它进行了 20 个时期的训练,但看一下损失曲线就可以看出,曲线仍在下降,模型可以再训练一些时期。让我们看看这些模型在它们最后的时代给出了什么样的验证精度。
MobileNetV2 acc: 0.9788306355476379
InceptionV3 acc: 0.9778226017951965
Xception acc: 0.9788306355476379
**Ensemble acc: 0.9828628897666931**
总体精度几乎提高了 0.5%,如果考虑到之前的精度为 97.8%,这是一个巨大的提高。
以这种方式创建集合模型是一个非常长的过程。它需要比单一模型多四倍的努力,然而,它可以帮助获得多一点的准确性,这是很难获得的,一旦我们达到 90 以上的准确性。下面你可以找到完整的代码。
在结束之前,我想对这篇文章给予一些肯定,它帮助我完成了这篇文章。
使用检测器 2 对象检测和分类跟踪来检测和跟踪棒球
当视频中有多个棒球时,我如何跟踪棒球。
棒球检测和跟踪。
介绍
在我的上一篇文章中,我使用 Detectron2 训练并构建了一个模型来检测视频中的棒球。
使用自定义数据集的 Train Detectron2 对象检测。
towardsdatascience.com](/how-to-build-a-baseball-detector-using-detectron2-50b44edec6b7)
使用探测器 2 的棒球探测。(来自我之前的帖子)
它工作得很好,能够在大多数画面中捕捉到球。然而,将该模型应用于真实棒球练习视频的一个潜在问题是,视频中可能有不止一个棒球,如第一幅图所示。这个问题使得很难提取精确球的信息,例如球的速度和飞行角度。
解决这个问题的一个可能的方法是跟踪球并给它们分配唯一的 id,然后我可以计算每个球的信息并选择我想要的球。
有几种方法来跟踪物体,我决定使用亚历克斯·比雷的排序(简单的在线和实时跟踪)。详细介绍和论文可以在作者的报告中找到。我将在这篇文章中重点讨论实现。
视频序列中 2D 多目标跟踪的简单在线实时跟踪算法。查看示例…
github.com](https://github.com/abewley/sort)
棒球追踪——排序
我使用了 Google Colab,所以我首先安装了我的 Google Drive,并将 sort.py 复制到这个文件夹中。然后我安装了排序和导入的需求。
!cp "gdrive/My Drive/Colab Notebooks/object_tracking/sort.py" .
!pip install filterpyfrom sort import *
然后我调用了排序对象
mot_tracker1 = Sort(max_age=3, min_hits=1, iou_threshold=0.15)
max_age、min_hist 和 iou_threshold 是可以根据要求进行调整的参数。
然后,我通过每帧更新跟踪器,将分类跟踪器集成到对象检测循环中。
track_bbs_ids = mot_tracker1.update(dets)
dets 是一个 numpy 检测数组,格式为[[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],…]。
它将返回一个数组[[x1,y1,x2,y2,ID],[x1,y1,x2,y2,ID],…]。ID 是由 SORT 分配的唯一焊球 ID。
必须注意的是,即使在检测到空帧的情况下,也必须对每一帧调用该函数。无检测时使用 np.empty((0,5))。
然后我通过绘制边界框和 id 来可视化它。
球跟踪。
结果挺好的。地面上的球被检测到并被分配了 id。然而,快速移动的球并没有通过排序分配给任何 ID。这可能是由于框架之间的球的距离太大,球的尺寸太小。框架之间的边界框没有重叠,因此排序将它们视为不同的球。
我试验了上面显示的排序参数,并做了一些变通来解决这个问题。
为了进行排序,我手动将边界框变大,因此帧之间的重叠变大。
球跟踪。
现在飞球可以被捕获并分配到一个唯一的 ID。
最后,我可以反转并可视化真实的边界框。我把盒子转换成圆形,这样更适合球形的球。我还为球添加了一个尾巴来显示轨迹。
球跟踪
看起来不错!在这一点上,我有每一帧的球检测 ID。在探测循环期间,我存储在一个列表中,并将其转换为 pandas dataframe 以供进一步分析。
熊猫数据框中的球检测细节。
接下来:
- 检测效果不是很好,一些球没有被识别出来,还有一些噪音,比如白手套,鞋子等等。被鉴定为鲍尔。我需要扩展训练数据集并改进检测模型。
- 检测速度有点慢,也许可以尝试不同的架构。
- 从侧面拍摄视频,这样我就可以在不考虑失真的情况下计算速度和角度。
感谢阅读,欢迎反馈和建议!
在这里支持我:https://medium.com/@c.kuan/membership
检测后院的动物——深度学习的实际应用。
这篇文章基于我在真实(和农村)生活情况中应用现有深度学习技术的经验。
我将展示如何将深度学习技术应用到实践中,并从中获得乐趣。
您可以从 my Github repo 获得完整代码。
后院发现浣熊
动机
你可能已经从我以前的帖子中了解到,我是一个“自然爱好者”,当我们不在的时候,我总是想知道我们的乡间别墅后院发生了什么。狐狸到处游荡或浣熊在屋顶上行走的谣言是真实的还是只是故事?
牢记这一点,我安装了夜视运动启动摄像机对准我们的后院,并开始捕捉拜访我们的夜间物种。
首先,我在回顾自己的录音,发现即使每个人都睡着了,那里的生活仍在继续。相机证明,狐狸和浣熊是相当频繁的访客,知道我想保持跟踪他们。但是我不能每晚都录自己的节目,只挑选有趣的。
一台摄像机拍摄半年视频。因为树的移动导致了大量的误报。
因此,我需要一个自动化系统来区分由于风或快速光照条件变化而导致的树木移动的假阳性和真实的动物移动。
我已经决定检验一下是否有可能从现有的开源技术中获得优势,而不需要成为这方面的专家,也不需要在任何云解决方案和硬件上花费大量金钱。
这些动物来自哪里?
大约三分之一的勃兰登堡州被 15 个自然保护区占据(1 个国家公园、3 个生物圈保护区和 11 个自然公园)
在的 Brandeburgs iNaturalist 页面上,你可以数出 23 种不同的哺乳动物,其中至少有 5 种是我在自家后院捕捉到的。
苍鹭落在后院。
其中至少有一只是松鼠。还是两者都有?
它是如何工作的?
- 捕捉: IP 户外摄像机捕捉运动并保存视频
- Store: 运行在 Raspberry Pi 3 上的 Small bash 通过 FTP 下载所有新视频,并保存到 NAS 存储中
- **分析:**使用 GPU 加速 Tensorflow 的 Python 应用程序对传入的视频文件进行推理,并在有检测的情况下生成新视频
- 分享: Python 应用更新 Telegram bot,发送新的检测视频
在接下来的部分中,我将经历每一步,并与你分享我的发现。
捕获
为了捕捉运动,我使用了两个小米/小米户外相机,通过黑客固件扩展了相机功能。这是一款非常可爱又实惠的相机,你可以在户外使用,不用担心会摔坏。
摄像头指向后院
被黑版本的固件增加了所需的功能,如 FTP 服务器和 RTSP 流,以便与家庭助理等软件集成
商店
这是一个简单的部分。
在我的本地 Raspberry Pi 上,我已经向 cron 添加了一个小型 bash 脚本,用于下载所有新的视频文件,并将它们存储到 USB“Lots-Of-TBs”驱动器中。
我根据这里的说明改编了一个脚本:https://askubuntu . com/questions/758640/how-to-automatically-sync-the-contents-of-a-local-folder-with-the-contents-of-a
用于从摄像机中提取视频的 Cron 脚本
分析
所有收到的文件都通过 OpenCV 进行处理,并使用 Tensorflow 进行分析
这是需要根据硬件对软件进行适当调整的部分。正如你可能理解的那样,在乡下,你不会让庞大的服务器保持运行,所以你需要从你负担得起的设备中获取最大的收益。
旅程的开始
- 安装 OpenCV
- 多重处理视频阅读器
- 张量流模型兆探测器
- 一批
- 可能的优化:图形优化,TensorRT
由于我没有数据、资源和时间来训练我自己的动物检测神经网络,我在网上搜索今天可用的内容。我发现,即使有最先进的神经网络和从世界各地收集的数据,这项任务也不像看起来那么简单。
当然也有做动物检测的产品和研究。尽管如此,与我所寻找的有一个主要区别——他们从照片相机或智能手机相机中检测生物,这样的照片在颜色、形状和质量上与你用运动检测相机获得的照片不同。
但是,不管怎样。仍然有一些项目和我的目标一样。我的搜索把我带到了微软的 CameraTraps 项目。据我所知,他们正在使用从世界各地不同的野生动物摄像机收集的数据构建图像识别 API。因此,他们开源了预先训练的模型,用于检测图像上是否出现“动物”或“人”,称为“ MegaDetector ”
该模型的主要限制来自模型的名称。它只是一个“探测器”,而不是一个“分类器”
微软关于检测器和分类器的声明
即使考虑到这样的限制,这样的方法确实非常适合我。
该模型被训练成检测三种不同的类别:
- 动物
- 人
- 车辆
浣熊被认定为“动物”类
在大多数情况下,当谈到视频对象检测时,您会在各种博客帖子中找到实时视频。我的情况有点不同——作为输入,我有一大堆由摄像机产生的视频文件,作为输出,我也想要视频文件。
用于在 Python 中读写视频文件——事实上被认为是 OpenCV 库。我也发现它是我最喜欢的图像处理包。
对视频文件进行推理的逻辑非常简单:
- **阅读:**从视频中获取一帧
- **检测:**对图像进行推断
- **写:**如果有检测,保存一帧视频到新文件。
- **重复:**运行步骤 1-3,直到视频结束
它可以用这个代码示例来实现
使用 OpenCV VideoReader 进行 Tensorflow 对象检测
尽管这种简单的方法在同一个线程读写时有几个瓶颈,但它是有效的。因此,如果你正在寻找一个代码来在视频上尝试你的模型,检查那个脚本。
我花了大约 10 分钟来处理一个全高清 1 分钟 10 FPS 的视频文件。
Detection took 9 minutes and 18.18 seconds. Average detection time per frame: 0.93 seconds
但是你可以找到很多这样的教程——告诉你如何运行一个普通的 OpenCV/Tensorflow 推理。具有挑战性的部分是如何让代码持续运行并具有良好的性能。
输入/输出块
根据提供的代码,读取帧、检测和写回都在同一个循环中发生,这意味着迟早有一个操作会成为瓶颈,例如,从“不太稳定”的网络存储中读取视频文件。
为了去掉这一部分,我使用了来自一个出色的计算机视觉博客 Adrian Rosebrock 和他的库 imutils 的指令。他提供了将读取帧和处理帧分割成多个线程的方法,这种方法为我提供了一个预先填充好的帧队列,准备进行处理。
它不会对推断时间产生太大影响,但它有助于慢速驱动器,这些驱动器通常用于视频存储。
优化:图形分析
我听说的另一部分是优化部署模型。我遵循了一个在这里发现的指南:https://towards data science . com/optimize-NVIDIA-gpu-performance-for-efficient-model-inference-F3 e 9874 e 9 FDC并通过分配非 GPU 支持的层在 CPU 上处理来实现一些改进。
[INFO] :: Detection took 8 minutes and 39.91 seconds. Average detection time per frame: 0.86 seconds
批量推断
根据我以前的经验,深度学习训练的瓶颈之一是从磁盘到 GPU 的数据传输,为了最大限度地减少时间,当 GPU 一次获得几个图像时,会使用所谓的“批处理”。
我想知道是否有可能对推理进行同样的批处理。幸运的是,根据 StackOverflow 的回答,这是可能的。
我只需要找到最大可接受的批量大小,并传递数组或帧进行推断。为此,我用批处理功能扩展了FileVideoStream
类
[INFO] :: Detection took 8 minutes and 1.12 second. Average detection time per frame: 0.8 seconds
优化:从源代码编译
当我们谈论运行繁重、耗时的计算时,另一个重要部分是从硬件中获取最大收益。
最直接的方法之一是使用机器类型的优化包。每位 Tensorflow 用户都看到过的信息:
tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX AVX2
这意味着 Tensorflow 由于忽略内置 CPU 优化而没有充分利用硬件。原因是安装了通用包,它可以在任何类型的 x86 机器上工作。
提高其性能的一种方法是安装来自第三方的优化包,如https://github.com/lakshayg/tensorflow-build、https://github.com/mind/wheels或https://github . com/yaroslavvb/tensor flow-community-wheels/issues
另一种方法是按照谷歌的指示,从源码https://www.tensorflow.org/install/source#tensorflow_1x编译这个包。但是考虑到,如果你以前没有经验,这可能有点困难,而且这是一个相当耗时耗内存的过程(上次在我的六核 CPU 上花了 3.5 小时)。
OpenCV 也是如此,但这是一个更复杂的主题,所以我不在这里讨论。Adrian Rosebrock 提供了一些方便的指南,如果你对这个话题感兴趣,请跟随他们。
分享
小 Python 应用程序等待带有检测的输入视频。当视频到达时,它更新了我的电报频道。我已经使用了我的以前的项目,它将传入的视频重新发送到我的电报频道。
该应用程序是这样配置的,并使用看门狗库持续监控文件夹中的新文件
{
"xiaomi_video_watch_dir" : PATH_TO_WATCH,
"xiaomi_video_temp_dir" : PATH_TO_STORE_TEMP_FILES,
"xiaomi_video_gif_dir" : PATH_WITH_OUTPUT_GIFS,
"tg_key" : TELEGRAM_KEY
}
初始电报组版本
什么没成功?
这个项目给我带来了很多新的知识,即使我已经设法达到了我的最终目标,我也经历了一些失败的尝试。我认为这是每个项目最重要的部分之一。
图像增强
在我的研究过程中,我看到了几份来自 iWildCam Kaggle 竞赛参与者的报告。他们经常提到将 CLAHE 算法应用于直方图均衡化的输入图像。我已经尝试了上述算法和其他几种算法,但没有成功。应用图像修改降低了成功检测的数量。但说实话,夜间摄像头的图像看起来更清晰。
**def** enchance_image(frame):
temp_img = frame
img_wb = wb.balanceWhite(temp_img)
img_lab = cv.cvtColor(img_wb, cv.COLOR_BGR2Lab)
l, a, b = cv.split(img_lab)
img_l = clahe.apply(l)
img_clahe = cv.merge((img_l, a, b))
return cv.cvtColor(img_clahe, cv.COLOR_Lab2BGR)
左图:CLAHE 应用版。右图:未增强
优化:TensorRT
出于某种原因,我无法遵循构建 tensort 引擎的指南,但是普通的 TF-tensort 文档工作得很好。
TF-TRT 模型优化
即使成功结束了优化,也没有发生推理加速。
[INFO] :: Detection took 8 minutes and 59.68 seconds. Average detection time per frame: 0.9 seconds
我的想法是,模型架构对于自动 TensorRT 引擎构建来说太深奥了。但是谁知道呢?
神奇加速
我有很多期望的另一个想法是从模型和图形优化中获得显著的加速。我期望得到接近文档和 YouTube 说明的结果(3-4 倍加速),但我只得到大约 1.3-1.5 倍,这当然比什么都没有好得多。
我对为什么加速没有如此戏剧性的想法如下:
- GPU 是一个瓶颈——没有足够的 VRAM 不会给快速计算留下太多空间。
- 用于 MegaDetector 的模型架构非常复杂(更快的 RCNN ),不适合自动优化
- 我做了错事,_(ツ)_/。但至少,我试过了。
充分利用 CPU 和 GPU
我不太喜欢的另一部分是 CPU 利用率。当 GPU 被计算堵塞时,CPU 消耗大约为 30%。我已经尝试给它一些额外的工作来预取视频帧,但它仍然没有在推理过程中发挥重要作用。
我希望从 Tensorflow 库中获得一些并行计算,但这似乎是我所能达到的最大程度。
我有一个想法,有两个并行的 Tensorflow 会话——一个用于 GPU,一个用于 CPU,但这有太多的工作要做。
未来计划
- 用神经网络发布 Docker 图像
- 与微软团队分享关于误报的发现
- 将代码打包到 python 包中
捕捉到的视频令人惊叹
这里给读完的读者一点小奖励:)
捕获和检测到的香料示例。