Image title

在 Elixir 中构建机器学习项目。

机器学习简介

机器学习是开发人员、企业、技术爱好者和普通公众日益增长的兴趣领域。从敏捷初创企业到引领潮流的行业领导者,企业都知道,成功实施正确的机器学习产品可以给他们带来巨大的竞争优势。

我们已经看到企业通过自动化聊天机器人和定制购物体验在生产中从机器学习中获得显著收益。

鉴于我们最近演示了如何在 Elixir 中完成 Web 抓取,我们认为我们会更进一步,向您展示如何在机器学习项目中应用它。

您可能还喜欢:
JavaScript 中的简单机器学习项目

经典算法方法与机器学习

传统方法始终以算法为中心。为此,您需要设计一个有效的算法来修复边缘情况并满足您的数据操作需求。数据集越复杂,覆盖所有角度就越难,在某些时候,算法不再是最佳方法。

幸运的是,机器学习提供了另一种选择。构建基于机器学习的系统时,目标是在数据中查找依赖关系。您需要正确的信息来训练程序,以解决它可能会被问到的问题。为了提供正确的信息,传入数据对于机器学习系统至关重要。您需要提供足够的培训数据集才能取得成功。

因此,如果没有进一步的演示,我们将为机器学习项目提供一个示例教程,并展示我们如何取得成功。请随意跟随。

项目描述

对于这个项目,我们将看看一个提供实时价格比较和建议的电子商务平台。任何电子商务机器学习项目的核心功能是:

  1. 从网站中提取数据
  2. 处理此数据
  3. 为客户提供情报和建议
  4. 可变步骤,取决于操作和学习
  5. 利润

最常见的问题之一是需要以一致的方式对数据进行分组。例如,假设我们想要统一所有男士时尚品牌的产品类别(因此,我们可以跨多个数据源呈现给定类别中的所有产品)。每个站点(因此数据源)可能具有不一致的结构和名称,这些结构和名称需要统一和匹配,然后才能进行准确的比较。

出于本指南的目的,我们将构建一个项目::

  1. 从一组网站中提取数据(在本例中,我们将演示如何从 harveynorman.ie 商店中提取数据)
  2. 训练神经网络以识别产品图像中的产品类别
  3. 将神经网络集成到 Elixir 代码中,以便完成图像识别并推荐产品
  4. 构建将所有内容粘合在一起的 Web 应用。

提取数据

正如我们在开头提到的,数据是任何成功的机器学习系统的基石。在此步骤中成功的关键是提取公开提供的真实数据,然后将其准备到培训集中).我们将使用提取的图像及其类别执行机器学习培训。

训练神经网络模型的质量与您提供的数据数据集的质量直接相关。因此,确保提取的数据真正有意义非常重要。

我们将使用名为Crawly的库来执行数据提取。

Crawly 是一个应用程序框架,用于对网站进行爬网和提取结构化数据,这些数据可用于各种有用的应用程序,如数据挖掘、信息处理或历史存档。你可以在文档页面上找到更多关于它。或者你可以访问我们的指南如何完成在Elixir的网页刮擦。

现在,我们来解释一下,让我们开始吧!首先,我们将创建一个新的 Elixir 项目:

现在创建项目,修改 deps mix.exs 文件的函数,因此如下所示:

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:crawly, "~> 0.1"},
    ]
  end

现在,获取所有依赖项: mix deps.get ,我们已准备就绪。下一步,实现负责抓取harveynorman.ie网站的模块。将以下代码保存在lib/products_advisor/spiders/harveynorman.ex

defmodule HarveynormanIe do
 @behaviour Crawly.Spider

 require Logger
 @impl Crawly.Spider
 def base_url(), do: "https://www.harveynorman.ie"

 @impl Crawly.Spider
 def init() do
  [
   start_urls: [
    "https://www.harveynorman.ie/tvs-headphones/"
   ]
  ]
 end

 @impl Crawly.Spider
 def parse_item(response) do

    # Extracting pagination urls
  pagination_urls =
   response.body |> Floki.find("ol.pager li a") |> Floki.attribute("href")

    # Extracting product urls
  product_urls =
   response.body |> Floki.find("a.product-img") |> Floki.attribute("href")

  all_urls = pagination_urls ++ product_urls

    # Converting URLs into Crawly requests
  requests =
   all_urls
   |> Enum.map(&build_absolute_url/1)
   |> Enum.map(&Crawly.Utils.request_from_url/1)

  # Extracting item fields
  title = response.body |> Floki.find("h1.product-title") |> Floki.text()
  id = response.body |> Floki.find(".product-id") |> Floki.text()

  category =
   response.body
   |> Floki.find(".nav-breadcrumbs :nth-child(3)")
   |> Floki.text()

  description =
   response.body |> Floki.find(".product-tab-wrapper") |> Floki.text()

  images =
   response.body
   |> Floki.find(" .pict")
   |> Floki.attribute("src")
   |> Enum.map(&build_image_url/1)

  %Crawly.ParsedItem{
   :items => [
    %{
     id: id,
     title: title,
     category: category,
     images: images,
     description: description
    }
   ],
   :requests => requests
  }
 end

 defp build_absolute_url(url), do: URI.merge(base_url(), url) |> to_string()

 defp build_image_url(url) do
  URI.merge("https://hniesfp.imgix.net", url) |> to_string()
 end

end

在这里,我们实现一个名为的 HarveynormanIe Crawly.Spider 模块,它通过定义其回调触发 init/0 行为:(用于创建蜘蛛代码用于获取初始页面的初始 base_url/0 请求),(用于筛选出不相关的 urls 内容,例如 urlsparse_item/1和(负责将下载的请求转换为项目和要遵循的新请求)管道.验证,
爬虫.管道.重复过滤器,
爬行.管道.JSON编码器
]

就是这样。我们的基本爬网程序已准备就绪,现在我们可以获取以 JL 格式提取的数据,并发送到名称下的文件夹:/tmp/HarveynormanIe.jl

Crawly 支持多种配置选项,例如base_store_path允许您将项目存储在不同位置,请参阅此处的文档的相关部分。对 Crawly 功能的完整审查超出了本博客文章的范围。

使用以下命令启动蜘蛛:

iex -S mix
Crawly.Engine.start_spider(HarveynormanIe)

您将在日志中看到以下条目:

6:34:48.639 [debug] Scraped "{\"title\":\"Sony MDR-E9LP In-Ear Headphones | Blue\",\"images\":[\"https://hniesfp.imgix.net/8/images/detailed/161/MDRE9LPL.AE.jpg?fit=fill&bg=0FFF&w=833&h=555&auto=format,compress\",\"https://hniesfp.imgix.net/8/images/feature_variant/48/sony_logo_v3.jpg?fit=fill&bg=0FFF&w=264&h=68&auto=format,compress\"],\"id\":\"MDRE9LPL.AE\",\"description\":\"Neodymium Magnet13.5mm driver unit reproduces powerful bass sound.Pair with a Music PlayerUse your headphones with a Walkman, "<> ...

上述条目指示爬网进程已成功运行,并且我们正在获取存储在文件系统中的项。

张力流模型训练

为了简化和加快模型训练过程,我们将使用预先训练的图像分类器。我们将使用在 ImageNet 上训练的图像分类器在使用传输学习技术之上创建新的分类图层。新型号将基于 MobileNet V2,深度乘数为 0.5,输入大小为 224×224 像素。

此部分基于 TensorFlow教程,有关如何重新训练新类别的图像分类器。如果按照上述步骤执行,则训练数据集已下载(刮伤)到配置的目录(默认情况下为/tmp/products_advisor)。所有图像都根据其类别进行定位:

/tmp/products_advisor  
├── building_&_hardware  
├── computer_accessories  
├── connected_home  
├── headphones  
├── hi-fi,_audio_&_speakers  
├── home_cinema  
├── lighting_&_electrical  
├── storage_&_home  
├── tools  
├── toughbuilt_24in_wall_organizer  
├── tv_&_audio_accessories  
└── tvs

在训练模型之前,让我们查看下载的数据集。您可以看到,某些类别包含非常少量的擦除图像。在图像少于 200 个的情况下,没有足够的数据来准确训练机器学习程序,因此我们可以删除这些类别。

find /tmp/products_advisor -depth 1 -type d \
        -exec bash -c "echo -ne '{}'; ls '{}' | wc -l" \; \
    | awk '$2<200 {print $1}' \
    | xargs -L1 rm -rf

这将留给我们只有 5 个类别可用于新模型:

/tmp/products_advisor  
├── headphones  
├── hi-fi,_audio_&_speakers  
├── tools  
├── tv_&_audio_accessories  
└── tvs

创建模型与运行由 TensorFlow 作者创建的 python 脚本一样简单,可以在官方的 TensorFlow Github存储库中找到:

TFMODULE=https://tfhub

py |
–tfhub_module_$TFMODULE |
–bottleneck_dir\tf/瓶颈 |
–how_many_training_steps=1000 |
–model_dir_tf/模型 |
–summaries_dir/training_summaries |
-output_graph_tf/retrained_graph.pb |
–output_labels\tf/retrained_labels.txt |
–image_dir\/tmp/products_advisor

在 MacBook Pro 2018 2.2 GHz 英特尔酷睿 i7 上,此过程大约需要 5 分钟。因此,重新训练的图形以及新标签类别可以在配置的位置(tf/retrained_graph.pb 和 tf/retrained_labels.txt)中找到,这些可用于进一步的图像分类:

IMAGE_PATH="/tmp/products_advisor/hi-fi,_audio_&_speakers/0017c7f1-129f-4fa7-a62b-9766d2cb4486.jpeg"

python bin/label_image.py \
    --graph=tf/retrained_graph.pb \
    --labels tf/retrained_labels.txt \
    --image=$IMAGE_PATH \
    --input_layer=Placeholder \
    --output_layer=final_result \
    --input_height=224 \
    --input_width=224

hi-fi audio speakers 0.9721675
tools 0.01919974
tv audio accessories 0.008398962
headphones 0.00015944676
tvs 7.433378e-05

正如您所看到的,新训练的模型对训练集中的图像进行了分类,其概率为 0.9721675,属于”高保真音频扬声器”类别。

使用灵丹妙药进行图像分类

使用 python 可以使用以下代码创建张数:

import tensorflow as tf

def read_tensor_from_image_file(file_name):
    file_reader = tf.read_file("file_reader", input_name)
    image_reader = tf.image.decode_jpeg(
        file_reader, channels=3, name="jpeg_reader")
    float_caster = tf.cast(image_reader, tf.float32)
    dims_expander = tf.expand_dims(float_caster, 0)
    resized = tf.image.resize_bilinear(dims_expander, [224, 224])
    normalized = tf.divide(resized, [input_std])
    sess = tf.Session()
    return sess.run(normalized)

现在,让我们对 Elixir 应用程序中的图像进行分类。TensorFlow 为以下语言提供 API:Python、C++、Java、Go 和 JavaScript。显然,BEAM 语言没有本机支持。我们可以使用C++绑定,尽管C++库仅用于 bazel 生成工具。

让我们把与 bazel 的混合集成作为练习留给好奇的读者,而看看 C API,它可用作 Elixir 的本机实现函数 (NIF)。幸运的是,没有必要为 Elixir 编写绑定,因为有一个库几乎拥有我们需要的一切:https://github.com/anshuman23/tensorflex

正如我们前面看到的,要将图像作为 TensorFlow 会话的输入提供,必须将其转换为可接受的格式:4 维张量,其中包含大小为 224×224 的解码规范化图像(如所选的 MobileNet V2 模型中定义)。输出是一个二维张量,可以保存值矢量。对于新训练的模型,输出以 5x1 float32 张量的形式接收。5 来自模型中的类数。

图像解码

假设图像将在 JPEG 中提供编码。我们可以在 Elixir 中编写一个库来解码 JPEG,但是,有几个开源 C 库可以从 NIN 中使用。另一个选项是搜索已提供此功能的 Elixir 库。Hex.pm显示,有一个名为 imago 的库可以解码不同格式的图像并执行一些后处理。它使用铁锈,并依赖于其他锈库来执行其解码。在我们的例子中,几乎所有的功能都是多余的。为了减少依赖项的数量并用于教育目的,让我们将此划分为 2 个简单的 Elixir 库,这些库将负责 JPEG 解码和图像大小调整这使得 Elixir 成为库中负责加载 NIF 和记录 API 的一部分:

defmodule Jaypeg do
  @moduledoc 
  Simple library for JPEG processing.

  ## Decoding

   elixir
  {:ok, <<104, 146, ...>>, [width: 2000, height: 1333, channels: 3]} =
      Jaypeg.decode(File.read!("file/image.jpg"))



  @on_load :load_nifs

  @doc 
  Decode JPEG image and return information about the decode image such
  as width, height and number of channels.

  ## Examples

      iex> Jaypeg.decode(File.read!("file/image.jpg"))
      {:ok, <<104, 146, ...>>, [width: 2000, height: 1333, channels: 3]}


  def decode(_encoded_image) do
    :erlang.nif_error(:nif_not_loaded)
  end

  def load_nifs do
    :ok = :erlang.load_nif(Application.app_dir(:jaypeg, "priv/jaypeg"), 0)
  end
End

NIF 实现并不复杂。它初始化解码 JPEG 变量所需的一切,将图像提供的内容作为流传递到 JPEG 解码器,并最终自行清理:

static ERL_NIF_TERM decode(ErlNifEnv *env, int argc,
                           const ERL_NIF_TERM argv[]) {
  ERL_NIF_TERM jpeg_binary_term;
  jpeg_binary_term = argv[0];
  if (!enif_is_binary(env, jpeg_binary_term)) {
    return enif_make_badarg(env);
  }

  ErlNifBinary jpeg_binary;
  enif_inspect_binary(env, jpeg_binary_term, &jpeg_binary);

  struct jpeg_decompress_struct cinfo;
  struct jpeg_error_mgr jerr;
  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_decompress(&cinfo);

  FILE * img_src = fmemopen(jpeg_binary.data, jpeg_binary.size, "rb");
  if (img_src == NULL)
    return enif_make_tuple2(env, enif_make_atom(env, "error"),
                            enif_make_atom(env, "fmemopen"));

  jpeg_stdio_src(&cinfo, img_src);

  int error_check;
  error_check = jpeg_read_header(&cinfo, TRUE);
  if (error_check != 1)
    return enif_make_tuple2(env, enif_make_atom(env, "error"),
                            enif_make_atom(env, "bad_jpeg"));

  jpeg_start_decompress(&cinfo);

  int width, height, num_pixels, row_stride;
  width = cinfo.output_width;
  height = cinfo.output_height;
  num_pixels = cinfo.output_components;
  unsigned long output_size;
  output_size = width * height * num_pixels;
  row_stride = width * num_pixels;

  ErlNifBinary bmp_binary;
  enif_alloc_binary(output_size, &bmp_binary);

  while (cinfo.output_scanline < cinfo.output_height) {
    unsigned char *buf[1];
    buf[0] = bmp_binary.data + cinfo.output_scanline * row_stride;
    jpeg_read_scanlines(&cinfo, buf, 1);
  }

  jpeg_finish_decompress(&cinfo);
  jpeg_destroy_decompress(&cinfo);

  fclose(img_src);

  ERL_NIF_TERM bmp_term;
  bmp_term = enif_make_binary(env, &bmp_binary);
  ERL_NIF_TERM properties_term;
  properties_term = decode_properties(env, width, height, num_pixels);

  return enif_make_tuple3(
    env, enif_make_atom(env, "ok"), bmp_term, properties_term);
}

现在,要使工具工作完成的所有工作就是声明 NIF 函数和定义。完整的代码在GitHub上可用。

图像调整

尽管可以使用 Elixir 重新实现图像操作算法,但这已不在本练习的范围之内,我们决定使用 C/C++ stb库,该库分布在公共域下,并可轻松集成为 Elixir NIF。该库实际上只是调整图像大小的 C 函数的代理,Elixir 部分专用于 NIF 加载和文档:

static ERL_NIF_TERM resize(ErlNifEnv *env, int argc,
                           const ERL_NIF_TERM argv[]) {
  ErlNifBinary in_img_binary;
  enif_inspect_binary(env, argv[0], &in_img_binary);

  unsigned in_width, in_height, num_channels;
  enif_get_uint(env, argv[1], &in_width);
  enif_get_uint(env, argv[2], &in_height);
  enif_get_uint(env, argv[3], &num_channels);

  unsigned out_width, out_height;
  enif_get_uint(env, argv[4], &out_width);
  enif_get_uint(env, argv[5], &out_height);

  unsigned long output_size;
  output_size = out_width * out_height * num_channels;
  ErlNifBinary out_img_binary;
  enif_alloc_binary(output_size, &out_img_binary);

  if (stbir_resize_uint8(
        in_img_binary

数据、out_width、out_height、0、num_channels)!= 1)
返回enif_make_tuple2(
Env
enif_make_atom(env,”错误”),”
enif_make_atom(env,”调整大小”);

ERL_NIF_TERMout_img_term;
out_img_term = enif_make_binary(out_img_binary);

返回enif_make_tuple2(env,enif_make_atom(env,”ok”),out_img_term);
}

GitHub上也提供了大小调整库。

从图像创建张力

现在是时候从处理过的图像创建张子了(在解码和调整大小之后)。为了能够将处理的图像加载为张量,Tensorflex 库应扩展为两个函数:

  1. 从提供的二进制文件创建矩阵
  2. 从给定矩阵创建浮子32张量。

函数的实现非常特定于 Tensorflex,如果不了解上下文,对读者没有多大意义。NIF 实现可以在GitHub上找到,并且可以在函数 binary_to_matrixmatrix_to_float32_tensor 和分别找到。

把所有东西放在一起

一旦所有必要的组件都可用,就该将所有组件放在一起了。此部分类似于博客文章开头看到的内容,其中图像使用 Python 进行标记,但这次我们将使用 Elixir 来利用我们修改的所有库:

def classify_image(image, graph, labels) do
    {:ok, decoded, properties} = Jaypeg.decode(image)
    in_width = properties[:width]
    in_height = properties[:height]
    channels = properties[:channels]
    height = width = 224

    {:ok, resized} =
      ImgUtils.resize(decoded, in_width, in_height, channels, width, height)

    {:ok, input_tensor} =
      Tensorflex.binary_to_matrix(resized, width, height * channels)
      |> Tensorflex.divide_matrix_by_scalar(255)
      |> Tensorflex.matrix_to_float32_tensor({1, width, height, channels})

    {:ok, output_tensor} =
      Tensorflex.create_matrix(1, 2, [[length(labels), 1]])
      |> Tensorflex.float32_tensor_alloc()

    Tensorflex.run_session(
      graph,
      input_tensor,
      output_tensor,
      "Placeholder",
      "final_result"
    )
  end

classify_image函数返回每个给定标签的概率列表:

iex(1)> image = File.read!("/tmp/tv.jpeg")
<<255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 255,
  219, 0, 132, 0, 9, 6, 7, 19, 19, 18, 21, 18, 19, 17, 18, 22, 19, 21, 21, 21,
  22, 18, 23, 19, 23, 18, 19, 21, 23, ...>>
iex(2)> {:ok, graph} = Tensorflex.read_graph("/tmp/retrained_graph.pb")
{:ok,
 %Tensorflex.Graph{
   def: #Reference<0.2581978403.3326476294.49326>,
   name: "/Users/grigory/work/image_classifier/priv/retrained_graph.pb"
 }}
iex(3)> labels = ImageClassifier.read_labels("/tmp/retrained_labels.txt")
["headphones", "hi fi audio speakers", "tools", "tv audio accessories", "tvs"]
iex(4)> probes = ImageClassifier.classify_image(image, graph, labels)
[
  [1.605743818799965e-6, 2.0029481220262824e-6, 3.241990925744176e-4,
   3.040388401132077e-4, 0.9993681311607361]
]

retrained_graph.pb可在 retrained_labels.txt 模型培训步骤前面提到的产品-顾问-模型-培训师存储库的 tf 目录中找到。如果模型训练成功,tf 目录应与此树类似:

/products-advisor-model-trainer/tf/  
├── bottlenecks  
├── retrained_graph.pb  
├── retrained_labels

展展(探针)>枚举.zip(标签)>枚举.max()
{0.9993681311607361,”tvs”}

了解更多信息

所以,你有它。这是如何使用 Elixir 完成机器学习项目的基本演示。完整的代码在GitHub上可用。如果您想了解更多此类项目,为什么不注册我们的时事通讯?或看看我们的详细博客如何完成在Elixir的网页刮擦。或者,如果您正在规划一个机器学习项目,为什么不与我们交谈,我们很乐意为您提供帮助。

进一步阅读

机器学习项目无法投入生产的 6 个原因

开发人员需要了解 SDLC 中的机器学习

Comments are closed.