机器人的人工视觉和语言处理-全-

机器人的人工视觉和语言处理(全)

原文:annas-archive.org/md5/f77f73a89c331afcaea234799b391074

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

关于

本节简要介绍了作者、本书的内容、你开始时所需的技术技能,以及完成所有包括的活动和练习所需的硬件和软件要求。

本书介绍

机器人学中的人工视觉与语言处理 书籍首先讨论了机器人的理论。你将比较不同的机器人工作方法,探索计算机视觉、其算法和局限性。然后你将学习如何通过自然语言处理命令来控制机器人。在这本书的过程中,你将学习 Word2Vec 和 GloVe 嵌入技术、非数值数据以及循环神经网络(RNN)及其高级模型。你将使用 Keras 创建一个简单的 Word2Vec 模型,构建一个卷积神经网络(CNN),并通过数据增强和迁移学习来改进它。你将学习 ROS 并构建一个对话代理来管理你的机器人。你还将把你的代理与 ROS 集成,并将图像转换为文本,文本转换为语音。你将学习如何借助视频剪辑构建一个物体识别系统。

本书结束时,你将具备构建一个功能性应用程序的技能,该应用程序可以与 ROS 集成,从你的环境中提取有用的信息。

作者介绍

Álvaro Morena Alberola 是一名计算机工程师,热爱机器人技术和人工智能。目前,他正在担任软件开发人员。他对 AI 的核心部分——人工视觉非常感兴趣。Álvaro 喜欢使用新技术并学习如何利用先进工具。他认为机器人技术是改善人类生活的一种方式,是帮助人们完成他们自己无法完成的任务的一种手段。

Gonzalo Molina Gallego 是一名计算机科学毕业生,专注于人工智能和自然语言处理。他有丰富的文本对话系统工作经验,创建过对话代理,并提供过良好的方法论建议。目前,他正在研究混合领域对话系统的新技术。Gonzalo 认为对话用户界面是未来的发展趋势。

Unai Garay Maestre 是一名计算机科学毕业生,专注于人工智能和计算机视觉领域。他曾成功地为 2018 年 CIARP 会议贡献了一篇论文,提出了使用变分自编码器的新数据增强方法。他还从事机器学习开发工作,使用深度神经网络处理图像。

目标

  • 探索 ROS 并构建一个基本的机器人系统

  • 使用 NLP 技术识别对话意图

  • 学习并使用 Word2Vec 和 GloVe 的词嵌入

  • 使用深度学习实现人工智能(AI)和物体识别

  • 开发一个简单的物体识别系统,使用 CNN

  • 将 AI 与 ROS 集成,使你的机器人能够识别物体

读者群体

人工视觉和语言处理技术在机器人学中的应用 针对想要学习如何集成计算机视觉和深度学习技术以创建完整机器人系统的机器人工程师。如果你具备 Python 的工作知识和深度学习背景,那将会很有帮助。对 ROS 的了解是一个加分项。

方法

人工视觉和语言处理技术在机器人学中的应用 采用实用方法,为你提供了创建集成计算机视觉和自然语言处理控制机器人系统的工具。本书分为三个部分:自然语言处理、计算机视觉和机器人学。它在详细介绍基础知识后引入高级主题。书中还包含多个活动,供你在高度相关的背景下练习和应用你的新技能。

最低硬件要求

为了最佳的学生体验,我们建议以下硬件配置:

  • 处理器:2GHz 双核处理器或更好

  • 内存:8 GB RAM

  • 存储空间:5 GB 可用硬盘空间

  • 良好的互联网连接

为了训练神经网络,我们建议使用 Google Colab。但如果你想用自己的计算机训练这些网络,你需要:

  • NVIDIA GPU

软件要求

不推荐在本书中使用 Ubuntu 16.04,因为它与 ROS Kinetic 存在兼容性问题。但如果你想使用 Ubuntu 18.04,有一个 ROS 支持的版本,名为 Melodic。在项目进行过程中,你需要安装几个库以完成所有练习,如 NLTK (<= 3.4)、spaCy (<=2.0.18)、gensim (<=3.7.0)、NumPy (<=1.15.4)、sklearn (<=0.20.1)、Matplotlib (<=3.0.2)、OpenCV (<=4.0.0.21)、Keras (<=2.2.4) 和 Tensorflow (<=1.5, >=2.0)。每个库的安装过程在练习中有详细说明。

要在 Ubuntu 系统中使用 YOLO,你需要安装你的 GPU 的 NVIDIA 驱动程序和 NVIDIA CUDA 工具包。

约定

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟的 URL、用户输入和 Twitter 句柄显示如下:"使用 TfidfVectorizer 方法,我们可以将语料库中的文档集合转换为 TF-IDF 特征矩阵"

代码块如下所示:

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(corpus)

新术语和重要词汇以粗体显示。在屏幕上看到的词汇,例如菜单或对话框中的内容,会在文本中这样显示:"形态分析:专注于分析句子中的词和其形态素"

安装和设置

在你开始本书之前,你需要安装以下软件。你可以在这里找到安装步骤:

安装 Git LFS

为了从本书的 GitHub 下载所有资源并能够使用图像来训练你的神经网络模型,你需要安装 Git LFS(Git Large File Storage)。它用 Git 内部的文本指针替换大文件,如音频样本、视频、数据集和图形。

如果你还没有克隆存储库:

  1. 安装 Git LFS

  2. 克隆 Git 仓库

  3. 从仓库文件夹执行 gitlfs pull

  4. 完成

如果仓库已经克隆:

  1. 安装 Git LFS

  2. 从仓库文件夹执行:gitlfs pull

  3. 完成

安装 Git LFS:github.com/git-lfs/git-lfs/wiki/Installation

[推荐] Google Colaboratory

如果可能,使用 Google Colaboratory。它是一个免费的 Jupyter notebook 环境,无需设置,完全运行在云端。你还可以利用 GPU 来运行它。

使用它的步骤如下:

  1. 将整个 GitHub 上传到你的 Google Drive 账户中,这样你就可以使用存储在仓库中的文件。确保你先使用 Git LFS 加载了所有文件。

  2. 前往你想打开新 Google Colab Notebook 的文件夹,点击“新建”>“更多”>“Colaboratory”。现在,你有一个已打开并保存在相应文件夹中的 Google Colab Notebook,你可以开始使用 Python、Keras 或任何已安装的库。

  3. 如果你想安装特定的库,你可以使用“pip”包安装或其他命令行安装,并在开头加上“!”。例如,“!pip install sklearn”将安装 scikit-learn。

  4. 如果你想从 Google Drive 加载文件,你需要在 Google Colab 单元格中执行以下两行代码:

    from google.colab import drive
    drive.mount(‘drive’)
    
  5. 然后,打开输出中出现的链接,并使用你创建 Google Colab Notebook 时所用的 Google 账户登录。

  6. 现在,你可以使用ls列出当前目录中的文件,并使用cd导航到特定的文件夹,以便找到上传的文件位置。

  7. 现在,Google Colab Notebook 可以像在该文件夹中打开的 Jupyter notebook 一样加载任何文件并执行任何任务。

安装 ROS Kinetic

这是你必须遵循的步骤,以在你的 Ubuntu 系统上安装该框架:

  1. 准备 Ubuntu 接受 ROS 软件:

    sudosh -c ‘echo “deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main” > /etc/apt/sources.list.d/ros-latest.list’
    
  2. 配置下载密钥:

    sudo apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116
    
  3. 确保系统已更新:

    sudo apt-get update
    
  4. 安装完整框架,以免遗漏功能:

    sudo apt-get install ros-kinetic-desktop-full
    
  5. 初始化并更新 rosdep

    sudo rosdep init
    rosdep update
    
  6. 如果你不想每次工作时都声明环境变量,可以将它们添加到 bashrc 文件中:

    echo “source /opt/ros/kinetic/setup.bash” >> ~/.bashrcsource ~/.bashrc
    

    注意

    完成此过程后,可能需要重启计算机以使系统应用新的配置。

  7. 通过启动框架检查它是否正常工作:

    roscore
    

配置 TurtleBot

注意

可能会出现 TurtleBot 与你的 ROS 发行版不兼容的情况(我们使用的是 Kinetic Kame),但不用担心,Gazebo 中有很多机器人可以模拟。你可以查找不同的机器人并尝试在你的 ROS 发行版中使用它们。

这是 TurtleBot 的配置过程:

  1. 安装其依赖项:

    sudo apt-get install ros-kinetic-turtlebotros-kinetic-turtlebot-apps ros-kinetic-turtlebot-interactions ros-kinetic-turtlebot-simulator ros-kinetic-kobuki-ftdiros-kinetic-ar-track-alvar-msgs
    
  2. 在你的 catkin 工作空间中下载 TurtleBot 模拟器包:

    cd ~/catkin_ws/src
    git clone https://github.com/turtlebot/turtlebot_simulator
    
  3. 之后,您应该能够在 Gazebo 中使用 TurtleBot。

    如果在 Gazebo 中尝试可视化 TurtleBot 时遇到错误,请从我们的 GitHub 下载 turtlebot_simulator 文件夹并替换它。

    启动 ROS 服务:

    roscore
    

    启动 TurtleBot World:

    cd ~/catkin_ws
    catkin_make
    sourcedevel/setup.bash
    roslaunchturtlebot_gazeboturtlebot_world.launch
    

Darknet 基本安装

按照以下步骤安装 Darknet:

  1. 下载框架:

    git clone https://github.com/pjreddie/darknet
    
  2. 切换到下载的文件夹并运行编译命令:

    cd darknet
    make
    

    如果编译过程正确完成,您应该看到类似以下的输出:

Darknet 编译输出

Darknet 高级安装

这是您必须完成的安装过程,以实现章节目标。这将允许您使用 GPU 计算实时检测和识别物体。在执行此安装之前,您必须在 Ubuntu 系统上安装一些依赖项,如下所示:

  • NVIDIA 驱动程序:这些驱动程序将使您的系统能够正确地与 GPU 配合工作。正如您所知,它必须是 NVIDIA 型号。

  • CUDA:这是一个 NVIDIA 工具包,为构建需要 GPU 使用的应用程序提供开发环境。

  • OpenCV:这是一个免费的人工视觉库,非常适合处理图像。

    注意

    需要注意的是,所有这些依赖项都有多个版本。您必须找到与您的特定 GPU 和系统兼容的每个工具的版本。

    一旦系统准备就绪,您可以执行高级安装:

  1. 如果您还没有完成基本安装,请下载框架:

    git clone https://github.com/pjreddie/darknet
    
  2. 修改 Makefile 文件的前几行以启用 OpenCV 和 CUDA,内容应该如下:

    GPU=1
    CUDNN=0
    OPENCV=1
    OPENMP=0
    DEBUG=0
    
  3. 保存 Makefile 修改,切换到 darknet 目录并运行编译命令:

    cd darknet
    make
    

    现在,您应该看到类似以下的输出:

Darknet 编译带有 CUDA 和 OpenCV

安装 YOLO

在执行此安装之前,您必须在 Ubuntu 系统上安装一些依赖项,如 Darknet 的高级安装 部分所述。

注意

需要考虑的是,所有这些依赖项都有多个版本。您必须找到与您的特定 GPU 和系统兼容的每个工具的版本。

一旦系统准备好,您可以执行高级安装:

  1. 下载框架:

    git clone https://github.com/pjreddie/darknet
    
  2. 修改 Makefile 文件的前几行以启用 OpenCV 和 CUDA,内容应该如下:

    GPU=1
    CUDNN=0
    OPENCV=1
    OPENMP=0
    DEBUG=0
    
  3. 保存 Makefile 修改,切换到 darknet 目录并运行编译命令:

    cd darknet
    Make
    

额外资源

本书的代码包也托管在 GitHub 上,地址为:github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics

我们还有其他代码包,来自我们丰富的书籍和视频目录,可以在github.com/PacktPublishing/找到。快来看看吧!

文档链接:

ROS Kinetic - wiki.ros.org/kinetic/Installation

Git 大文件存储 - git-lfs.github.com/

第二章:第一章

机器人学基础

学习目标

在本章结束时,你将能够:

  • 描述机器人学历史中的重要事件

  • 解释使用人工智能、人工视觉和自然语言处理的重要性

  • 根据目标或功能对机器人进行分类

  • 识别机器人的各个部分

  • 使用里程计估算机器人的位置

本章概述了机器人学的简短历史,分类了不同类型的机器人及其硬件,并解释了如何使用里程计来找到机器人的位置。

介绍

机器人学领域代表着人类的现在和未来。目前,工业部门、研究实验室、大学,甚至我们的家庭中都存在机器人。机器人学学科正在不断发展,这也是它值得学习的原因之一。每个机器人都需要有人为其编程。即使是基于人工智能和自我学习的机器人,也需要设置初始目标。发生故障的机器人需要技术人员进行维护,基于人工智能的系统也需要持续的数据输入和监控才能有效运作。

在本书中,你将学习和实践许多有趣的技术,重点是人工计算机视觉、自然语言处理,以及与机器人和模拟器的互动。这将为你在机器人学的一些前沿领域打下坚实的基础。

机器人学的历史

机器人学源于创造智能机器以执行人类难以完成的任务的需求。但最初并没有被称为“机器人学”。“机器人”这一术语由捷克作家卡雷尔·恰佩克在他的作品R.U.R.罗索姆的万能机器人)中创造。它源自捷克语单词robota,意为奴役,指的是强迫劳动。

Čapek 的作品在全球广为人知,"机器人"一词也因此变得家喻户晓,以至于著名的教师和作家艾萨克·阿西莫夫在他的作品中也使用了这个术语;他将机器人学定义为研究机器人及其特征的科学。

这里你可以看到塑造机器人历史的重要事件的时间线:

图 1.1:机器人历史

图 1.1:机器人历史

图 1.2:机器人历史(续)

图 1.2:机器人历史(续)

图 1.1 和 1.2 提供了一个有用的时间线,展示了机器人学的起源和发展。

人工智能

人工智能指的是一系列旨在赋予机器与人类相同能力的算法。它使机器人能够做出自己的决策、与人类互动,并识别物体。这种智能不仅存在于机器人中,还广泛应用于许多其他领域和系统中(尽管人们可能并未察觉到它)。

已经有许多现实世界中的产品正在使用这种技术。这里列出了一些示例,展示了你可以构建的有趣应用程序:

  • Siri:这是由苹果公司创建的语音助手,内置于他们的手机和平板电脑中。Siri 非常实用,因为它连接到互联网,可以即时查找数据、发送信息、查看天气等,功能非常丰富。

  • Netflix:Netflix 是一个在线视频影视服务。它基于用户观看历史,通过 AI 开发的非常准确的推荐系统向用户推荐电影。例如,如果用户通常观看浪漫电影,系统将推荐浪漫剧集和电影。

  • Spotify:Spotify 是一个类似于 Netflix 的在线音乐服务。它使用推荐系统,根据用户的听歌历史和库中添加的音乐类型,向用户精准推荐歌曲。

  • 特斯拉的自动驾驶汽车:这些汽车使用 AI 技术,能够检测障碍物、人类以及交通信号,以确保乘客安全出行。

  • Pacman:像几乎所有其他视频游戏一样,Pacman 的敌人是通过 AI 编程的。它们使用一种特定的技术,不断计算碰撞距离,考虑到墙壁边界,并试图困住 Pacman。由于这是一个非常简单的游戏,算法并不复杂,但它很好地展示了 AI 在娱乐中的重要性。

自然语言处理

自然语言处理NLP)是人工智能的一个专业领域,涉及研究使人类与机器之间实现沟通的各种方式。它是唯一能够使机器人理解和再现人类语言的技术。

如果用户使用的是一款本应具备交流能力的应用程序,用户便会期望该应用能进行类人对话。如果类人机器人使用语句不规范或没有给出与问题相关的答案,用户体验将不佳,机器人也不会成为吸引人的购买选项。因此,理解并充分利用自然语言处理(NLP)在机器人学中的重要性不言而喻。

让我们来看看一些使用自然语言处理的现实应用:

  • Siri:苹果的语音助手 Siri 使用自然语言处理来理解用户的语音,并给出有意义的回应。

  • Cortana:这是微软创建的另一个语音助手,集成在 Windows 10 操作系统中。它的工作方式与 Siri 相似。

  • Bixby:Bixby 是三星公司的一部分,集成在最新的三星手机中,用户体验与使用 Siri 或 Cortana 类似。

    注意事项

    你可能会问这三者中哪个最好;然而,这取决于每个用户的喜好和需求。

  • 电话客服:如今,客户服务电话通常由自动语音应答系统接听。这些系统大多数是通过接收关键词输入的电话运营商。现代的电话运营商大多使用自然语言处理技术,能够与客户进行更真实的电话对话。

  • Google Home:Google 的虚拟家居助手使用自然语言处理技术来回答用户的问题并执行指定的任务。

计算机视觉

计算机视觉是机器人技术中常用的一种技术,它可以使用不同的摄像头模拟人眼的生物力学三维运动。可以定义为一套用于获取、分析和处理图像的方法,并将其转化为计算机可以处理的有价值信息。这意味着收集到的信息会转化为数字数据,以便计算机可以进行处理。这将在接下来的章节中介绍。

这里列出了一些使用计算机视觉的现实世界例子:

  • 自动驾驶汽车:自动驾驶汽车使用计算机视觉来获取交通和环境信息,并根据这些信息决定接下来的行动。例如,如果汽车的摄像头捕捉到行人穿过,它就会停车。

  • 手机相机应用:许多手机上的相机应用程序包括能修改拍摄照片的效果。例如,Instagram允许用户在实时中使用滤镜,通过将用户的面部与滤镜映射来修改图像。

  • 网球鹰眼:这是一种基于计算机的视觉系统,用于网球比赛中追踪球的轨迹,并在场地上显示其最可能的路径。它被用来检查球是否在场地边界内弹跳。

机器人的类型

讨论人工智能和自然语言处理时,了解现实世界中的机器人非常重要,因为这些机器人可以给你一个关于现有模型发展和改进的清晰印象。但首先,让我们来讨论一下我们能找到的不同类型的机器人。通常,它们可以分为工业机器人和服务机器人,我们将在接下来的章节中进行讨论。

工业机器人

工业机器人用于制造过程,通常没有人形。一般来说,它们看起来很像其他机器。这是因为它们的设计目的是执行特定的工业任务。

服务机器人

服务机器人以部分或完全自主的方式工作,执行对人类有用的任务。这些机器人还可以进一步分为两类:

  • 个人机器人:这些机器人通常用于繁琐的家务清洁任务,或娱乐行业。这是人们在讨论机器人时常常想象的那种机器,它们通常被想象成具有类人特征的机器人。

  • 田间机器人:这些机器人负责军事和探索任务。它们使用耐用材料建造,因为它们必须承受严酷的阳光和其他外部天气因素。

在这里你可以看到一些现实世界中个人机器人的例子:

  • Sophia:这是由汉森机器人公司创造的人形机器人。它的设计目的是与人类共同生活并向他们学习。

  • Roomba:这是由 iRobot 公司生产的清洁机器人。它由一个带轮子的圆形底座组成,可以在房子里移动,同时计算出最有效的方式来覆盖整个区域。

  • Pepper:Pepper 是由 SoftBank Robotics 设计的社交机器人。虽然它具有人形,但并不像人类那样双足行走。它还配备有一个轮式底座,提供良好的移动性。

机器人硬件和软件

就像任何其他计算机系统一样,机器人由硬件和软件组成。机器人所使用的硬件和软件将取决于机器人的用途和设计它的开发者。然而,有一些硬件组件是多个机器人中较为常见的,我们将在本章中讨论这些组件。

首先,让我们来看一下每个机器人都有的三种组件:

  • 控制系统:控制系统是机器人的核心组件,负责连接所有需要控制的其他组件。它通常是一个微控制器或微处理器,其性能取决于机器人本身。

  • 执行器:执行器是机器人一部分,使其能够改变外部环境,例如用于移动整个机器人或机器人某部分的电机,或者用于发出声音的扬声器。

  • 传感器:这些组件负责获取信息,使机器人能够根据这些信息产生期望的输出。这些信息可以与机器人的内部状态或外部环境有关。基于此,传感器分为以下几种类型:

  • 内部传感器:其中大多数用于测量机器人的位置,因此你通常会在这些机器人身体内部找到它们。以下是一些机器人可以使用的内部传感器:

    光电传感器:这些是能够检测任何穿越传感器内部凹槽物体的传感器。

    编码器:编码器是一种可以将微小运动转化为电信号的传感器。这个信号随后被控制系统用来执行多种操作。例如,电梯中的编码器可以通知控制系统电梯是否到达正确楼层。通过计数编码器自转的次数,可以知道编码器提供的功率。这是将运动转化为一定能量的过程。

    信标和 GPS 系统:信标和 GPS 系统是用于估算物体位置的传感器。GPS 系统能够成功完成此任务,得益于它们从卫星获得的信息。

  • 外部传感器:这些传感器用于获取机器人周围环境的数据。它们包括接近传感器、接触传感器、光线传感器、颜色传感器、反射传感器和红外传感器。

    以下图示展示了机器人内部结构的图示:

图 1.3:机器人部件示意图

图 1.3:机器人部件示意图

为了更好地理解前面的示意图,我们将看到每个组件在模拟情况下是如何工作的。假设一个机器人接到命令,从 A 点移动到 B 点:

图 1.4:机器人从 A 点开始移动

图 1.4:机器人从 A 点开始移动

机器人使用内部传感器GPS,不断检查自身的位置,并检查是否已到达目标点。GPS 计算坐标并将其发送到控制系统,控制系统将处理这些数据。如果机器人尚未到达 B 点,控制系统会指示执行器继续前进。这个过程在下图中表示:

图 1.5:机器人正在完成从 A 到 B 的路径

图 1.5:机器人正在完成从 A 到 B 的路径

另一方面,如果 GPS 发送到控制系统的坐标与 B 点匹配,控制系统将指示执行器完成过程,之后机器人将停止移动:

图 1.6:路径结束!机器人到达 B 点

图 1.6:路径结束!机器人到达 B 点

机器人定位

通过使用前面一节提到的内部传感器之一,我们可以计算机器人在一定位移后的位置。这种计算叫做里程计,可以通过编码器及其提供的信息来完成。讨论这一技术时,需要记住主要的优点和缺点:

  • 优点:它可以在没有外部传感器的情况下计算机器人的位置,这样可以使机器人的设计成本大大降低。

  • 缺点:最终的位置计算并不完全准确,因为它依赖于地面和轮子的状态。

现在,让我们一步一步地看一下如何进行这种计算。假设我们有一台两轮移动的机器人,我们将按如下步骤进行:

  1. 首先,我们需要计算轮子行驶的距离,这可以通过使用从发动机的编码器中提取的信息来完成。在一个两轮机器人中,一个简单的示意图可能是这样的:图 1.7:两轮机器人运动的示意图

    图 1.7:两轮机器人运动的示意图

    左轮行驶的距离是图 1.6 中标记为 DL 的虚线,而 DR 代表右轮。

  2. 为了计算轮轴中心点的线性位移,我们需要第一步中计算的信息。使用相同的简单示意图,Dc 将是距离:

    注意

    如果你在使用多轴轮时,首先应该研究轴的分布,然后计算每个轴的行驶距离。

    图 1.8:两轮机器人运动的示意图(2)

    图 1.8:两轮机器人运动示意图 (2)
  3. 要计算机器人的旋转角度,我们需要在第一步中获得的最终计算结果。这里我们所指的角度是 α:图 1.9:两轮机器人运动示意图 (3)

    图 1.9:两轮机器人运动示意图 (3)

    如图所示,在这种情况下,α 将是 90º,这意味着机器人已经旋转了一定的角度。

  4. 一旦你获得了所有信息,就可以进行一系列的计算(将在下一节中讨论)来得到最终位置的坐标。

练习 1:计算机器人的位置

在这个练习中,我们使用之前的过程来计算两轮机器人在运动一定时间后的位置信息。首先,让我们考虑以下数据:

  • 轮径 = 10 cm

  • 机器人底座长度 = 80 cm

  • 每圈编码器计数 = 76

  • 每 5 秒钟左编码器的计数 = 600

  • 每 5 秒钟右编码器的计数 = 900

  • 初始位置 = (0, 0, 0)

  • 移动时间 = 5 秒

    注意

    每圈编码器计数是我们用来计算编码器每圈在其轴上完成一次旋转所产生的能量的单位。例如,在上面提供的信息中,左编码器在 5 秒内完成 600 次计数。我们也知道,编码器完成一圈需要 76 次计数。因此,我们可以推断,在 5 秒内,编码器将完成 7 圈(600/76)。这样,如果我们知道 1 圈所产生的能量,就可以计算出 5 秒内所产生的能量。

对于初始位置,第一个和第二个数字表示 X 和 Y 坐标,最后一个数字表示机器人的旋转角度。这些数据是相对的,你需要想象坐标轴的起点。

现在,让我们按照以下步骤进行:

  1. 让我们计算每个车轮的完成距离。我们首先计算每个编码器在运动过程中执行的计数次数。这可以通过将总运动距离除以给定的编码器时间,并乘以每个编码器的计数次数来轻松计算:

    (移动时间 / 编码器时间) * 左编码器计数:

    (5 / 5) * 600 = 600 次计数

    (移动时间 / 编码器时间) * 右编码器计数:

    (5 / 5) * 900 = 900 次计数

    计算出这个值后,我们可以利用这些数据来获取总距离。由于车轮是圆形的,我们可以通过以下方式计算每个车轮的已完成距离:

    [2πr / 每圈编码器计数] * 总左编码器计数:

    (10π/76) * 600 = 248.02 cm

    [2πr / 每圈编码器计数] * 总右编码器计数:

    (10π/76) * 900 = 372.03 cm

  2. 现在计算轮轴中心点的线性位移。这可以通过一个简单的计算来完成:

    (左轮距离 + 右轮距离) / 2:

    (248.02 + 372.03) / 2 = 310.03 cm

  3. 计算机器人的旋转角度。为此,你可以计算每个轮子完成的距离差,并将其除以底盘长度:

    (右轮距离 - 左轮距离)/ 底盘长度:

    (372.03 - 248.02) / 80 = 1.55 弧度

  4. 最后,我们可以通过分别计算每个组件来得出最终位置。以下是计算每个组件所需使用的公式:

    最终 x 位置 = 初始 x 位置 + (轮子轴位移 * 旋转角度余弦):

    0 + (310.03 * cos (1.55)) = 6.45

    最终 y 位置 = 初始 y 位置 + (轮子轴位移 * 旋转角度余弦):

    0 + (310.03 * sin (1.55)) = 309.96

    机器人最终旋转 = 初始机器人旋转 + 机器人旋转角度:

    0 + 1.55 = 1.55

所以,在这个过程之后,我们可以得出结论,机器人从 (0, 0, 0) 移动到了 (6.45, 309.96, 1.55)。

如何与机器人合作

就像任何其他软件开发一样,为机器人实现应用程序和程序的过程有许多不同的方法。

在接下来的章节中,我们将使用一些框架和技术,使我们能够抽象出具体问题,并开发出一个容易适应各种机器人和设备的解决方案。在本书中,我们将使用机器人操作系统ROS)来实现这一目标。

在我们开始与机器人合作之前,另一个需要考虑的问题是使用哪种编程语言。你肯定知道并且已经使用过一些编程语言,那么哪种语言最合适呢?这个问题的真实答案是没有特定的语言;它总是取决于眼前的问题。但是在我们的书中,由于我们将要进行的活动类型,我们将使用 Python。正如你可能知道的,它是一种解释型、高级、通用的编程语言,广泛用于 AI 和机器人技术。

通过使用 Python,就像其他编程语言一样,你可以开发你希望机器人具备的任何功能。例如,你可以给机器人编程,让它在检测到人时进行简单的问候。你也可以编程一个更复杂的功能,比如当机器人“听到”音乐时跳舞。

现在我们将通过一些练习和活动,向你介绍 Python 在机器人中的应用,如果你之前没有使用过它的话。

练习 2:使用 Python 计算轮子行驶的距离

在本练习中,我们将实现一个简单的 Python 函数,用于计算轮子行驶的距离,使用我们在练习 1中执行的相同过程,计算机器人的位置。以下是需要遵循的步骤:

  1. 导入所需资源。在本例中,我们将使用 π(圆周率):

    from math import pi
    
  2. 创建带有参数的函数。为了计算这个距离,我们需要以下内容:

    轮子的直径(以厘米为单位)

    每圈的编码器计数

    用来测量编码器计数的秒数

    给定时间内的轮子编码器计数

    总的移动时间

    这是函数定义:

    def wheel_distance(diameter, encoder, encoder_time, wheel, movement_time):
    
  3. 从函数的实现开始。首先,计算编码器测量的距离:

    time = movement_time / encoder_time
    wheel_encoder = wheel * time
    
  4. 将上面获得的距离转换为我们预期的距离,即轮子行驶的距离:

    wheel_distance = (wheel_encoder * diameter * pi) / encoder
    
  5. 返回最终值:

    return wheel_distance
    
  6. 最后,你可以通过向函数传递值并进行相应的手动计算来检查函数是否正确实现:

    wheel_distance(10, 76, 5, 400, 5)
    

    这个函数调用应该返回 165.34698176788385

你的笔记本中的输出应该像这样:

图 1.10:轮子最终覆盖的距离

图 1.10:轮子最终覆盖的距离

练习 3:使用 Python 计算最终位置

在本练习中,我们使用 Python 来计算机器人最终位置,给定机器人的初始位置、轴完成的距离和旋转角度。你可以按照以下过程来实现:

  1. 导入正弦和余弦函数:

    from math import cos, sin
    
  2. 使用所需的参数定义该函数:

    机器人初始位置(坐标)

    机器人中央轴完成的距离

    从初始点的角度变化:

    def final_position(initial_pos, wheel_axis, angle):
    

    通过编写 练习 1:计算机器人的位置 中使用的公式来设置一个函数。

    它们可以像这样编码:

    final_x = initial_pos[0] + (wheel_axis * cos(angle))
    final_y = initial_pos[1] + (wheel_axis * sin(angle))
    final_angle = initial_pos[2] + angle
    

    注意

    return(final_x, final_y, final_angle)
    
  3. 再次,你可以通过传入所有参数并手动计算结果来测试该函数:

    final_position((0,0,0), 125, 1)
    

    上面的代码返回以下结果:

    (67.53778823351747, 105.18387310098706, 1)
    

    在这里,你可以看到整个实现过程以及一个函数调用的示例:

图 1.11:计算出的机器人最终位置

图 1.11:计算出的机器人最终位置

活动 1:使用 Python 进行机器人定位(里程计法)

你正在创建一个系统,检测机器人在移动一定时间后的位置。开发一个 Python 函数,给定以下数据后返回机器人最终位置:

  • 轮子直径(厘米) = 10 厘米

  • 机器人底盘长度 = 80 厘米

  • 每圈的编码器计数 = 76

  • 用于测量编码器计数的秒数 = 600

  • 给定秒数内的左右编码器计数 = 900

  • 初始位置 = (0, 0, 0)

  • 移动持续时间(秒) = 5 秒

    注意

    之前练习中实现的函数可以帮助你完成本活动。你可以按照以下步骤继续进行此活动。

按照以下步骤将帮助你完成练习:

  1. 首先,你需要计算每个轮子完成的距离。

  2. 要继续,你需要计算轴所完成的距离。

  3. 现在计算机器人的旋转角度。

  4. 然后计算机器人的最终位置。

输出应该像这样:

图 1.11:通过活动的 Python 函数计算的机器人最终位置

图 1.11:通过活动的 Python 函数计算的机器人最终位置

注意:

本活动的解决方案可以在第 300 页找到。

总结

在本章中,你已了解了机器人技术的世界。你学习了如自然语言处理(NLP)和计算机视觉等先进技术,并将其与机器人技术结合使用。在本章中,你还使用了 Python,而在接下来的章节中你将继续使用它。

此外,你已经利用了里程计来计算机器人在没有外部传感器的情况下的位置。如你所见,如果所需数据可用,计算机器人的位置并不难。请注意,尽管里程计是一项不错的技术,但在未来的章节中,我们将使用其他方法,这些方法将使我们能够使用传感器,并且在结果的准确性上可能更优。

在接下来的章节中,我们将探讨计算机视觉,并处理一些更实际的主题。例如,你将学习机器学习、决策树和人工神经网络,并以应用它们于计算机视觉为目标。在接下来的章节中,你将会使用到这些技术,并且你肯定会有机会将它们应用于个人或职业用途。

第三章:第二章

计算机视觉简介

学习目标

到本章结束时,你将能够:

  • 解释人工智能和计算机视觉的影响

  • 部署一些基本的计算机视觉算法

  • 开发一些基本的机器学习算法

  • 构建你的第一个神经网络

本章介绍了计算机视觉的基本概念,接着介绍了一些重要的计算机视觉和机器学习基本算法。

介绍

人工智能AI)正在改变一切。它试图模拟人类智能,以完成各种任务。

处理图像的人工智能领域称为计算机视觉。计算机视觉是一个跨学科的科学领域,旨在模拟人类眼睛。它不仅从图像中提取像素并进行解读,还通过执行自动化任务和使用算法,从特定图像中获得更高层次的理解。

其中一些算法在物体识别、人脸识别、图像分类、图像编辑,甚至图像生成方面表现更好。

本章将从计算机视觉的介绍开始,首先讲解一些最基本的算法,并通过练习将它们付诸实践。接着,会介绍机器学习的基本算法到神经网络的概念,并通过多个练习来巩固所学的知识。

计算机视觉中的基本算法

在本节中,我们将讨论图像是如何形成的。我们将介绍一个非常有用的库,用于执行计算机视觉任务,并了解一些任务和算法的工作原理以及如何编码它们。

图像术语

要理解计算机视觉,我们首先需要了解图像是如何工作的以及计算机是如何解释它们的。

计算机将图像理解为一组数字的集合。更具体地说,图像被视为一个二维数组,一个包含从 0 到 255(在灰度图像中,0 代表黑色,255 代表白色)值的矩阵,表示图像的像素值(像素值),如下例所示:

图 2.1:没有像素值和有像素值的图像表示

图 2.1:没有像素值和有像素值的图像表示

在左侧的图像中,数字 3 以低分辨率显示。在右侧,显示了相同的图像,并附有每个像素的值。随着像素值的增加,颜色会变亮,而值减小时,颜色会变暗。

这张图像是灰度图像,这意味着它只是一个从 0 到 255 的二维数值数组,但彩色图像呢?彩色图像(或红/绿/蓝(RGB)图像)有三层二维数组堆叠在一起。每一层代表一种颜色,将它们组合在一起就形成了彩色图像。

上述图像的矩阵大小为 14x14 像素。在灰度模式下,它表示为 14x14x1,因为只有一个矩阵和一个通道。而对于 RGB 格式,它表示为 14x14x3,因为有三个通道。从中计算机只需理解这些图像是由这些像素构成的。

OpenCV

OpenCV 是一个开源的计算机视觉库,支持 C++、Python 和 Java 接口,并且支持 Windows、Linux、macOS、iOS 和 Android。

在本章中提到的所有算法,我们将使用 OpenCV。OpenCV 帮助我们通过 Python 实现这些算法。如果你想实践这些算法,建议使用 Google Colab。你需要安装 Python 3.5 或更高版本、OpenCV 和 NumPy,以便继续本章的内容。为了在屏幕上显示结果,我们将使用 Matplotlib。这两个库都是人工智能领域的优秀工具。

基本图像处理算法

为了让计算机理解图像,首先必须对图像进行处理。处理图像的算法有很多种,输出的结果取决于具体任务的要求。

一些最基本的算法包括:

  • 阈值化

  • 形态学变换

  • 模糊

阈值化

阈值化通常用于简化计算机和用户对图像的可视化方式,以便更容易进行分析。它基于用户设置的一个值,每个像素的值根据是否高于或低于设定值,转换为白色或黑色。如果图像是灰度图,输出图像将是黑白图像,但如果你选择保持 RGB 格式,阈值将应用于每个通道,这意味着图像仍然是彩色的。

有多种方法可以进行阈值化,以下是一些常用的阈值方法:

  1. 简单阈值化:如果像素值低于用户设定的阈值,则该像素将被赋值为 0(黑色)或 255(白色)。简单阈值化中也有不同的阈值化方式:

    阈值二进制

    阈值二进制反转

    截断

    阈值设为零

    阈值设为零反转

    不同类型的阈值如图 2.2 所示

    图 2.2:不同类型的阈值

    图 2.2:不同类型的阈值

    阈值二进制反转与二进制类似,但原本为黑色的像素变为白色,反之亦然。全局阈值化是简单阈值化下的另一种名称。

    截断显示阈值以上的像素值和实际像素值。

    阈值设为零时,如果像素值高于阈值,它将输出该像素的实际值,否则输出黑色图像,而阈值设为零反转则正好相反。

    注意

    阈值值可以根据图像或用户的需求进行调整。

  2. 自适应阈值法:简单阈值使用全局值作为阈值。如果图像某些部分的光照条件不同,算法的表现会比较差。在这种情况下,自适应阈值法会自动为图像的不同区域猜测不同的阈值,从而在不同光照条件下得到更好的整体效果。

    自适应阈值有两种类型:

    自适应均值阈值

    自适应高斯阈值

    自适应阈值与简单阈值的区别如图 2.3 所示

    图 2.3:自适应阈值和简单阈值的区别

    图 2.3:自适应阈值与简单阈值的区别

    在自适应均值阈值法中,阈值值是邻域区域的均值,而在自适应高斯阈值法中,阈值值是邻域值的加权和,其中权重是一个高斯窗口。

  3. 大津二值化法:在全局阈值法中,我们使用一个任意值作为阈值值。考虑一张双峰图像(像素分布在两个主要区域的图像)。你如何选择正确的阈值?大津二值化法会自动根据图像的直方图计算出适合双峰图像的阈值。图像直方图是一种直方图,它作为图形表示显示了色调数字图像中的分布:

图 2.4:大津阈值法

图 2.4:大津阈值法

练习 4:将不同的阈值应用于图像

注意

由于我们在 Google Colab 上训练人工神经网络,我们应该使用 Google Colab 提供的 GPU。为此,我们需要进入 runtime > Change runtime type > Hardware accelerator: GPU > Save

所有的练习和活动将主要在 Google Colab 中开发。除非另有指示,否则建议为不同的作业保持单独的文件夹。

Dataset 文件夹可以在 GitHub 的 Lesson02 | Activity02 文件夹中找到。

在这个练习中,我们将加载一张地铁图像,并应用阈值处理:

  1. 打开你的 Google Colab 界面。

  2. 创建一个书籍文件夹,下载 GitHub 上的 Dataset 文件夹,并将其上传到该文件夹中。

  3. 按如下方式导入驱动器并挂载:

    from google.colab import drive
    drive.mount('/content/drive')
    

    注意

    每次使用新协作者时,都需要将驱动器挂载到所需文件夹中。

    一旦你第一次挂载了驱动器,你将需要输入授权码,这个授权码可以通过点击 Google 提供的 URL 并按下键盘上的 Enter 键获得:

    图 2.5:显示 Google Colab 授权步骤的图像

    图 2.5:显示 Google Colab 授权步骤的图像
  4. 现在你已经挂载了驱动器,需要设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson02/Exercise04/
    

    注意

    第 5 步中提到的路径可能会根据你在 Google Drive 上的文件夹设置发生变化。路径总是以cd /content/drive/My Drive/开头。

    Dataset 文件夹必须出现在你设置的路径中。

  5. 现在你需要导入相应的依赖:OpenCV cv2 和 Matplotlib:

    import cv2
    from matplotlib import pyplot as plt
    
  6. 现在输入代码加载 subway.jpg 图像,我们将使用 OpenCV 对其进行灰度处理并使用 Matplotlib 显示:

    注意

    img = cv2.imread('subway.jpg',0)
    plt.imshow(img,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.6:绘制加载的地铁图像结果

    图 2.6:绘制加载的地铁图像结果
  7. 让我们通过使用 OpenCV 方法应用简单的阈值化处理。

    在 OpenCV 中执行此操作的方法称为 cv2.threshold,它需要三个参数:image(灰度图像)、threshold value(用于分类像素值的阈值),以及 maxVal,它表示当像素值大于(有时小于)阈值时所给出的值:

    _,thresh1 = cv2.threshold(img,107,255,cv2.THRESH_BINARY)
    _,thresh2 = cv2.threshold(img,107,255,cv2.THRESH_BINARY_INV) 
    _,thresh3 = cv2.threshold(img,107,255,cv2.THRESH_TRUNC) 
    _,thresh4 = cv2.threshold(img,107,255,cv2.THRESH_TOZERO)
    _,thresh5 = cv2.threshold(img,107,255,cv2.THRESH_TOZERO_INV) 
    titles = ['Original Image','BINARY', 'BINARY_INV', 'TRUNC','TOZERO','TOZERO_INV']
    images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
    for i in range(6):
        plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
        plt.title(titles[i])
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.7:使用 OpenCV 进行简单阈值化

    图 2.7:使用 OpenCV 进行简单阈值化
  8. 我们将对自适应阈值化做同样的操作。

    执行此操作的方法是 cv2.adaptiveThreshold,它有三个特殊的输入参数和一个输出参数。输入参数为自适应方法、块大小(邻域区域的大小)和 C(从计算得到的均值或加权均值中减去的常数),而输出参数只有阈值化后的图像。这与全局阈值化不同,后者有两个输出:

    th2=cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,71,7)
    th3=cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,71,7)
    titles = ['Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
    images = [th2, th3]
    for i in range(2):
        plt.subplot(1,2,i+1),plt.imshow(images[i],'gray')
        plt.title(titles[i])
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.8:使用 OpenCV 进行自适应阈值化

    图 2.8:使用 OpenCV 进行自适应阈值化
  9. 最后,让我们将 Otsu 二值化付诸实践。

  10. 该方法与简单的阈值化相同,cv2.threshold,只是多了一个额外的标志,cv2.THRESH_OTU

    ret2,th=cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    titles = ['Otsu\'s Thresholding']
    images = [th]
    for i in range(1):
        plt.subplot(1,1,i+1),plt.imshow(images[i],'gray')
        plt.title(titles[i])
        plt.xticks([]),plt.yticks([])
    plt.show()
    

图 2.9:使用 OpenCV 进行 Otsu 二值化

图 2.9:使用 OpenCV 进行 Otsu 二值化

现在你可以对任何图像应用不同的阈值化变换。

形态学变换

形态学变换由一组基于图像形状的简单图像操作组成,通常用于二值图像。它们通常用于区分文本与背景或其他形状。它们需要两个输入,一个是原始图像,另一个称为结构元素,它决定了操作的性质。通常是一个矩阵,它在图像上滑动,将其值与图像像素的值相乘。两个基本的形态学操作是腐蚀和膨胀。它们的变体形式是开运算和闭运算。应该使用哪种操作取决于具体任务:

  • 腐蚀:对于给定的二值图像,它会将图像的厚度在内部和外部各收缩一个像素,这些像素由白色像素表示。此方法可以多次应用。根据你想要实现的目标,该方法可以用于不同的目的,但通常它与膨胀一起使用(如图 2.10 所示),用于去除孔洞或噪声。这里展示的是腐蚀的示例,数字是 3:

图 2.10:腐蚀示例

图 2.10:腐蚀示例
  • 膨胀:该方法与腐蚀相反。它通过在二值图像的内部和外部各增加一个像素来增加物体的厚度。该方法也可以对图像多次应用。根据你想要实现的目标,这种方法可以用于不同的目的,但通常它与腐蚀结合使用,以去除图像中的孔洞或噪声。下面是膨胀的示例(我们已对图像应用了多次膨胀操作):

图 2.11:膨胀示例

图 2.11:膨胀示例
  • 开运算:该方法首先进行腐蚀,然后进行膨胀,通常用于去除图像中的噪声。

  • 闭运算:该算法与开运算相反,首先进行膨胀再进行腐蚀。它通常用于去除物体中的孔洞:

图 2.12:开运算和闭运算示例

图 2.12:开运算和闭运算示例

如你所见,开运算方法可以去除图像中的随机噪声,而闭运算方法则能有效修复图像中的小随机孔洞。为了去除开运算输出图像中的孔洞,可以应用闭运算方法。

还有更多的二值操作,但这些是基本操作。

练习 5:将各种形态学变换应用于图像

在本练习中,我们将加载一个数字图像,并对其应用我们刚刚学到的形态学变换:

  1. 打开你的 Google Colab 界面。

  2. 设置目录路径:

    cd /content/drive/My Drive/C13550/Lesson02/Exercise05/
    

    注意

    步骤 2 中提到的路径可能会发生变化,具体取决于你在 Google Drive 上的文件夹设置。

  3. 导入 OpenCV、Matplotlib 和 NumPy 库。NumPy 是 Python 科学计算的基础包,将帮助我们创建应用的卷积核:

    import cv2
    import numpy as np
    from matplotlib import pyplot as plt
    
  4. 现在输入代码,加载我们将使用 OpenCV 处理并通过 Matplotlib 显示的Dataset/three.png图像:

    注意

    img = cv2.imread('Dataset/three.png',0)
    plt.imshow(img,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.savefig('ex2_1.jpg', bbox_inches='tight')
    plt.show()
    

    图 2.13:加载图像的绘制结果

    图 2.13:加载图像的绘制结果
  5. 让我们使用 OpenCV 方法应用腐蚀操作。

    这里使用的方法是cv2.erode,它有三个参数:图像、在图像上滑动的卷积核和迭代次数,表示执行的次数:

    kernel = np.ones((2,2),np.uint8)
    erosion = cv2.erode(img,kernel,iterations = 1)
    plt.imshow(erosion,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.savefig('ex2_2.jpg', bbox_inches='tight')
    plt.show()
    

    图 2.14:使用 OpenCV 的腐蚀方法的输出结果

    图 2.14:使用 OpenCV 的腐蚀方法的输出结果

    如我们所见,图形的厚度减少了。

  6. 我们将对膨胀进行相同的操作。

    这里使用的方法是cv2.dilate,它有三个参数:图像、内核和迭代次数:

    kernel = np.ones((2,2),np.uint8)
    dilation = cv2.dilate(img,kernel,iterations = 1)
    plt.imshow(dilation,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.savefig('ex2_3.jpg', bbox_inches='tight')
    plt.show()
    

    图 2.15:使用 OpenCV 的膨胀方法的输出结果

    图 2.15:使用 OpenCV 的膨胀方法的输出结果

    如我们所见,图形的厚度增加了。

  7. 最后,让我们把开运算和闭运算应用到实践中。

    这里使用的方法是cv2.morphologyEx,它有三个参数:图像、应用的方法和内核:

    import random
    random.seed(42)
    def sp_noise(image,prob):
        '''
        Add salt and pepper noise to image
        prob: Probability of the noise
        '''
        output = np.zeros(image.shape,np.uint8)
        thres = 1 - prob 
        for i in range(image.shape[0]):
            for j in range(image.shape[1]):
                rdn = random.random()
                if rdn < prob:
                    output[i][j] = 0
                elif rdn > thres:
                    output[i][j] = 255
                else:
                    output[i][j] = image[i][j]
        return output
    def sp_noise_on_figure(image,prob):
        '''
        Add salt and pepper noise to image
        prob: Probability of the noise
        '''
        output = np.zeros(image.shape,np.uint8)
        thres = 1 - prob 
        for i in range(image.shape[0]):
            for j in range(image.shape[1]):
                rdn = random.random()
                if rdn < prob:
                    if image[i][j] > 100:
                        output[i][j] = 0
                else:
                    output[i][j] = image[i][j]
        return output
    kernel = np.ones((2,2),np.uint8) 
    # Create thicker figure to work with
    dilation = cv2.dilate(img, kernel, iterations = 1)
    # Create noisy image
    noise_img = sp_noise(dilation,0.05)
    # Create image with noise in the figure
    noise_img_on_image = sp_noise_on_figure(dilation,0.15)
    # Apply Opening to image with normal noise
    opening = cv2.morphologyEx(noise_img, cv2.MORPH_OPEN, kernel)
    # Apply Closing to image with noise in the figure
    closing = cv2.morphologyEx(noise_img_on_image, cv2.MORPH_CLOSE, kernel)
    images = [noise_img,opening,noise_img_on_image,closing]
    for i in range(4):
        plt.subplot(1,4,i+1),plt.imshow(images[i],'gray')
        plt.xticks([]),plt.yticks([])
    plt.savefig('ex2_4.jpg', bbox_inches='tight')
    plt.show()
    

图 2.16:使用 OpenCV 的开运算方法(左)和闭运算方法(右)的输出结果

图 2.16:使用 OpenCV 的开运算方法(左)和闭运算方法(右)的输出结果

注意

整个代码文件可以在 GitHub 的 Lesson02 | Exercise05 文件夹中找到。

模糊(平滑)

图像模糊通过滤波器内核在图像上执行卷积,简而言之,就是在图像的每一部分上乘以特定值的矩阵,以平滑图像。它有助于去除噪声和边缘:

  • 均值滤波:在这种方法中,我们考虑一个盒子滤波器或内核,它计算内核区域内像素的平均值,通过卷积将中央元素替换为整个图像的平均值。

  • 高斯模糊:这里应用的内核是高斯内核,而不是盒子滤波器。它用于去除图像中的高斯噪声。

  • 中值模糊:类似于均值滤波,但它用内核像素的中位数值替代中央元素。它对去除椒盐噪声(即图像中可见的黑白斑点)有很好的效果。

在图 2.17 中,我们应用了上述方法:

图 2.17:不同模糊方法对比的结果

图 2.17:不同模糊方法对比的结果

还有许多其他算法可以应用,但这些是最重要的。

练习 6:将各种模糊方法应用于图像

在这个练习中,我们将加载一张地铁图像,并对其应用模糊方法:

  1. 打开你的 Google Colab 界面。

  2. 设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson02/Exercise06/
    

    注意

    第 2 步中提到的路径可能会根据你在 Google Drive 上的文件夹设置有所不同。

  3. 导入 OpenCV、Matplotlib 和 NumPy 库:

    import cv2
    from matplotlib import pyplot as plt
    import numpy as np
    
  4. 输入代码以加载我们将要处理的Dataset/subway.png图像,使用 OpenCV 将其转换为灰度图像,并用 Matplotlib 显示:

    注意

    img = cv2.imread('Dataset/subway.jpg')
    #Method to convert the image to RGB
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.imshow(img)
    plt.savefig('ex3_1.jpg', bbox_inches='tight')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.18:以 RGB 格式绘制加载的地铁图像的结果

    图 2.18:以 RGB 格式绘制加载的地铁图像的结果
  5. 让我们应用所有的模糊方法:

    应用的方法有cv2.blurcv2.GaussianBlurcv2.medianBlur。它们都以图像作为第一个参数。第一种方法只接受一个参数,即内核。第二种方法需要内核和标准差(sigmaX 和 sigmaY),如果这两个参数都为零,则根据内核大小计算。最后提到的方法只需再加一个参数,即内核大小:

    blur = cv2.blur(img,(51,51)) # Apply normal Blurring
    blurG = cv2.GaussianBlur(img,(51,51),0) # Gaussian Blurring
    median = cv2.medianBlur(img,51) # Median Blurring
    titles = ['Original Image','Averaging', 'Gaussian Blurring', 'Median Blurring']
    images = [img, blur, blurG, median]
    for i in range(4):
        plt.subplot(2,2,i+1),plt.imshow(images[i])
        plt.title(titles[i])
        plt.xticks([]),plt.yticks([])
    plt.savefig('ex3_2.jpg', bbox_inches='tight')
    plt.show()
    

图 2.19:使用 OpenCV 的模糊方法

图 2.19:使用 OpenCV 的模糊方法

现在你已经知道如何将几种模糊技术应用于任何图像。

练习 7:加载图像并应用已学方法

在这个练习中,我们将加载一张数字图像,并应用我们到目前为止学到的方法。

注意

整个代码可以在 GitHub 的 Lesson02 | Exercise07-09 文件夹中找到。

  1. 打开一个新的 Google Colab 界面,并按照本章练习 4中提到的方法,挂载你的 Google Drive。

  2. 设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson02/Exercise07/
    

    注意

    第 2 步中提到的路径可能根据你在 Google Drive 上的文件夹设置有所不同。

  3. 导入相应的依赖项:NumPy、OpenCV 和 Matplotlib:

    import numpy as np  #Numpy
    import cv2          #OpenCV
    from matplotlib import pyplot as plt #Matplotlib
    count = 0
    
  4. 输入代码加载 Dataset/number.jpg 图像,我们将使用 OpenCV 将其处理为灰度图像,并使用 Matplotlib 显示:

    注意

    img = cv2.imread('Dataset/number.jpg',0)
    plt.imshow(img,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.20:加载带数字的图像结果

    图 2.20:加载带数字的图像结果
  5. 如果你想使用机器学习或任何其他算法来识别这些数字,你需要简化它们的可视化。使用阈值处理似乎是进行此操作的第一步。我们已经学习了一些阈值处理方法,但最常用的就是大津二值化法,因为它能够自动计算阈值,而不需要用户手动提供细节。

    对灰度图像应用大津二值化,并使用 Matplotlib 显示:

    _,th1=cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU
    th1 = (255-th1) 
    # This step changes the black with white and vice versa in order to have white figures
    plt.imshow(th1,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.21:在图像上使用大津二值化阈值处理

    图 2.21:在图像上使用大津二值化阈值处理
  6. 为了去除背景中的线条,我们需要进行一些形态学变换。首先,从应用闭操作方法开始:

    open1 = cv2.morphologyEx(th1, cv2.MORPH_OPEN, np.ones((4, 4),np.uint8))
    plt.imshow(open1,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.22:应用闭操作方法

    图 2.22:应用闭操作方法

    注意

    背景中的线条已经完全去除,现在数字的预测会更加容易。

  7. 为了填补这些数字中可见的空洞,我们需要应用开操作方法。对前面的图像应用开操作方法:

    close1 = cv2.morphologyEx(open1, cv2.MORPH_CLOSE, np.ones((8, 8), np.uint8))
    plt.imshow(close1,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.23:应用开操作方法

    图 2.23:应用开操作方法
  8. 数字周围仍然有一些杂质和不完美的地方。为了去除这些,使用更大内核的闭操作方法会是最佳选择。现在应用相应的方法:

    open2 = cv2.morphologyEx(close1, cv2.MORPH_OPEN,np.ones((7,12),np.uint8))
    plt.imshow(open2,cmap='gray')
    plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.24: 使用更大大小的核应用闭运算方法

    图 2.24: 使用更大大小的核应用闭运算方法

    根据你用于预测数字的分类器或给定图像的条件,可能会应用其他算法。

  9. 如果你想预测数字,你需要逐一进行预测。因此,你应该将数字分解为更小的数字。

    幸运的是,OpenCV 有一个方法可以实现这一点,它叫做cv2.findContours。为了找到轮廓,我们需要将黑色反转为白色。这个代码块较大,但只有在你想要逐个字符进行预测时才需要使用:

    _, contours, _ = cv2.findContours(open2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #Find contours
    cntsSorted = sorted(contours, key=lambda x: cv2.contourArea(x), reverse=True) #Sort the contours
    cntsLength = len(cntsSorted)
    images = []
    for idx in range(cntsLength): #Iterate over the contours
    	x, y, w, h = cv2.boundingRect(contour_no) #Get its position and size
    	... # Rest of the code in Github
    	images.append([x,sample_no]) #Add the image to the list of images and the X position
    images = sorted(images, key=lambda x: x[0]) #Sort the list of images using the X position
    {…}
    

    注意

    带有注释的完整代码可在 GitHub 上的 Lesson02 | Exercise07-09 文件夹中找到。

图 2.25: 提取的数字作为输出

图 2.25: 提取的数字作为输出

在代码的第一部分,我们正在寻找图像的轮廓(连接所有边界上连续点的曲线,这些点的颜色或强度相同),以找到每个数字,之后我们根据每个轮廓(每个数字)的区域进行排序。

接下来,我们遍历轮廓,使用给定的轮廓裁剪原始图像,最终将每个数字裁切成不同的图像。

接下来,我们需要让所有的图像具有相同的形状,因此我们使用 NumPy 将图像调整为给定的形状,并将图像与 X 位置一起添加到图像列表中。

最后,我们根据 X 位置对图像列表进行排序(从左到右,这样它们就保持顺序),并绘制结果。我们还将每个数字保存为单独的图像,以便之后可以单独使用每个数字进行任何任务。

恭喜!你已经成功处理了一张包含文本的图像,提取出了文本并且分离了每个字符,现在机器学习的魔法可以开始了。

机器学习简介

机器学习ML)是让计算机从数据中学习而不需要定义规则的科学。机器学习主要基于通过大量数据训练的模型,例如数字图像或不同物体的特征,并与它们相应的标签一起使用,如数字的数量或物体的类型。这被称为有监督学习。还有其他类型的学习,例如无监督学习强化学习,但我们将重点关注有监督学习。监督学习和无监督学习的主要区别在于,模型从数据中学习聚类(具体的聚类数量取决于你指定的聚类数),这些聚类会被转化为类别。而强化学习则关注软件代理如何在环境中采取行动,以增加奖励,奖励在代理执行正确操作时为正,反之为负。

在本章的这一部分,我们将理解机器学习并检查各种模型和算法,从最基本的模型到解释人工神经网络。

决策树和提升算法

在本节中,我们将解释决策树和提升算法作为最基本的机器学习算法之一。

装袋(决策树和随机森林)和提升(AdaBoost)将在本主题中进行解释。

装袋:

决策树或许是最基本的机器学习算法,用于分类和回归,但基本上主要用于教学和进行测试。

在决策树中,每个节点表示正在训练的数据的属性(是否为真),每个分支(节点之间的线)表示一个决策(如果某事为真,则选择这个方向;否则,选择另一个方向),每个叶子表示最终的结果(如果所有条件满足,则是一朵向日葵或雏菊)。

现在我们将使用鸢尾花数据集。该数据集考虑萼片宽度和长度以及花瓣宽度和长度,以便将鸢尾花分类为山鸢尾、变色鸢尾或维吉尼亚鸢尾。

可以使用 Python 从 scikit-learn 下载鸢尾花数据集:

scikit-learn.org/stable/modules/generated/sklearn.datasets.load_iris.html

Scikit-learn 是一个提供数据挖掘和数据分析有用工具的库。

下面的流程图显示了在这个数据集上训练的决策树的学习表示。X 代表数据集中的特征,X0 代表萼片长度,X1 代表萼片宽度,X2 代表花瓣长度,X3 代表花瓣宽度。'value'标签表示每个类别的样本落入每个节点的数量。我们可以看到,在第一步中,决策树仅通过考虑 X2 特征,花瓣长度,就能区分 setosa 与其他两个类别:

图 2.26:鸢尾花数据集的决策树图

图 2.26:鸢尾花数据集的决策树图

由于 scikit-learn,可以只用几行代码在 Python 中实现决策树:

from sklearn.tree import DecisionTreeClassifier
dtree=DecisionTreeClassifier()
dtree.fit(x,y)

xy分别是训练集的特征和标签。

x,除了仅代表这些长度和宽度的数据列,还可以是图像的每个像素。在机器学习中,当输入数据是图像时,每个像素被视为一个特征。

决策树是针对一个特定任务或数据集进行训练的,不能被转移到另一个类似的问题上。尽管如此,可以将多个决策树组合起来以创建更大的模型,并学习如何泛化。这些被称为随机森林

"森林"这个名字指的是多种决策树算法的集合,遵循袋装法,即多个算法的组合能够取得最佳的整体结果。出现“随机”一词是因为该算法在选择特征来分割节点时具有随机性。

再次感谢 scikit-learn,我们可以通过几行代码实现随机森林算法,代码与前面非常相似:

from sklearn.ensemble import RandomForestClassifier
rndForest=RandomForestClassifier(n_estimators=10)
rndForest.fit(x,y)

n_estimators表示底层决策树的数量。如果你使用这个方法测试结果,结果一定会有所提高。

还有其他一些方法也遵循提升法的方法论。提升法包含了所谓的弱学习器,这些学习器被组合成加权和,从而生成一个强学习器,并给出输出。这些弱学习器是顺序训练的,也就是说,每个学习器都会尝试解决前一个学习器所犯的错误。

有许多算法使用这种方法,最著名的有 AdaBoost、梯度提升和 XGBoost。我们这里只看 AdaBoost,因为它是最著名且最容易理解的。

提升法

AdaBoost将多个弱学习器组合在一起,形成一个强学习器。AdaBoost 的名字代表自适应提升,意味着该策略在每个时刻的权重是不同的。在一次迭代中被错误分类的例子,会在下一次迭代中得到更高的权重,反之亦然。

该方法的代码如下:

from sklearn.ensemble import AdaBoostClassifier
adaboost=AdaBoostClassifier(n_estimators=100)
adaboost.fit(x_train, y_train)

n_estimators是提升完成后的最大估算器数量。

这个方法的初始化是基于决策树的,因此其性能可能不如随机森林。但为了构建一个更好的分类器,应该使用随机森林算法:

AdaBoostClassifier(RandomForestClassifier(n_jobs=-1,n_estimators=500,max_features='auto'),n_estimators=100)

练习 8:使用决策树、随机森林和 AdaBoost 算法预测数字

在这个练习中,我们将使用上一练习中获得的数字和我们在本主题中学习到的模型来正确预测每个数字。为此,我们将从Dataset/numbers文件夹中的一些样本中提取几个数字,并结合 MNIST 数据集以获得足够的数据,从而使模型能够正确学习。MNIST 数据集由手写数字组成,数字范围从 0 到 9,形状为 28 x 28 x 3,主要供研究人员测试他们的方法或进行实验。然而,即使这些数字不完全相同,它也能帮助预测某些数字。你可以在yann.lecun.com/exdb/mnist/查看这个数据集。

由于安装 Keras 需要 TensorFlow,我们建议使用 Google Colab,它类似于 Jupyter Notebook,不同之处在于,它不会占用你的本地系统资源,而是使用远程虚拟机,并且所有机器学习和 Python 相关的库已经预安装好了。

让我们开始这个练习:

注意

我们将在本笔记本中继续从练习 7 的代码。

  1. 前往 Google Colab 界面,在那里你执行了练习 7加载图像并应用已学方法

  2. 导入库:

    import numpy as np
    import random
    from sklearn import metrics
    from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.utils import shuffle
    from matplotlib import pyplot as plt
    import cv2
    import os
    import re
    random.seed(42)
    

    注意

    我们将随机方法的种子设为 42,以保证可重复性:所有随机步骤具有相同的随机性,始终给出相同的输出。它可以设定为任何不变的数字。

  3. 现在我们将导入 MNIST 数据集:

    from keras.datasets import mnist
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    

    在代码的最后一行,我们加载了数据到x_train,即训练集(60,000 个数字示例),y_train,即这些数字的标签,x_test,即测试集,和y_test,即相应的标签。这些数据是 NumPy 格式的。

  4. 我们使用 Matplotlib 来展示其中一些数字:

    for idx in range(5):
        rnd_index = random.randint(0, 59999)
        plt.subplot(1,5,idx+1),plt.imshow(x_train[idx],'gray')
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.27: MNIST 数据集

    图 2.27: MNIST 数据集

    注意

    这些数字看起来和我们在上一个练习中提取的数字不同。为了使模型能够正确预测第一练习中处理过的图像中的数字,我们需要将一些这些数字添加到数据集中。

    这是添加新数字的过程,这些数字看起来像我们想要预测的数字:

    添加一个包含从 0 到 9 编号的子文件夹的 Dataset 文件夹(已完成)。

    获取前一个练习中的代码。

    使用代码提取存储在'Dataset/numbers/'中的所有数字(已完成)。

    将生成的数字粘贴到相应的文件夹中,文件夹名称与生成的数字对应(已完成)。

    将这些图像添加到原始数据集中(此练习中的步骤 5)。

  5. 要将这些图像添加到训练集中,应该声明以下两个方法:

    # ---------------------------------------------------------
    def list_files(directory, ext=None):
        return [os.path.join(directory, f) for f in os.listdir(directory)
                if os.path.isfile(os.path.join(directory, f)) and ( ext==None or re.match('([\w_-]+\.(?:' + ext + '))', f) )]
       # -------------------------------------------------------
    def load_images(path,label):
        X = []
        Y = []
        label = str(label)
        for fname in list_files( path, ext='jpg' ): 
            img = cv2.imread(fname,0)
            img = cv2.resize(img, (28, 28))
            X.append(img)
            Y.append(label)
        if maximum != -1 :
            X = X[:maximum]
            Y = Y[:maximum]
        X = np.asarray(X)
        Y = np.asarray(Y)
        return X, Y
    

    第一个方法,list_files(),列出文件夹中所有具有指定扩展名的文件,在本例中是jpg

    在主方法load_images()中,我们从这些文件夹中加载图像,这些图像来自数字文件夹,并附带相应的标签。如果最大值与-1 不同,我们会设定一个加载每个数字的数量的限制。这样做是因为每个数字应有相似的样本。最后,我们将列表转换为 NumPy 数组。

  6. 现在我们需要将这些数组添加到训练集中,以便我们的模型可以学习如何识别提取的数字:

    print(x_train.shape)
    print(x_test.shape)
    X, Y = load_images('Dataset/%d'%(0),0,9)
    for digit in range(1,10):
      X_aux, Y_aux = load_images('Dataset/%d'%(digit),digit,9)
      print(X_aux.shape)
      X = np.concatenate((X, X_aux), axis=0)
      Y = np.concatenate((Y, Y_aux), axis=0)
    

    使用前面代码中声明的方法添加这些数字后,我们将这些数组与前面创建的集合连接:

    from sklearn.model_selection import train_test_split
    x_tr, x_te, y_tr, y_te = train_test_split(X, Y, test_size=0.2)
    

    之后,使用sklearn中的train_test_split方法将这些数字分开—20%用于测试,其余部分用于训练:

    x_train = np.concatenate((x_train, x_tr), axis=0)
    y_train = np.concatenate((y_train, y_tr), axis=0)
    x_test = np.concatenate((x_test, x_te), axis=0)
    y_test = np.concatenate((y_test, y_te), axis=0)
    print(x_train.shape)
    print(x_test.shape)
    

    完成后,我们将这些数据与原始的训练集和测试集进行合并。我们在合并之前和之后打印了x_trainx_test的形状,因此可以看到那额外的 60 个数字。形状从(60,000, 28, 28)和(10,000, 28, 28)变为(60,072, 28, 28)和(10,018, 28, 28)。

  7. 对于我们将在本练习中使用的从 sklearn 导入的模型,我们需要将数组格式化为形状(n 个样本和数组),目前我们有的是(n 个样本,数组高度和数组宽度):

    x_train = x_train.reshape(x_train.shape[0],x_train.shape[1]*x_train.shape[2])
    x_test = x_test.reshape(x_test.shape[0],x_test.shape[1]*x_test.shape[2])
    print(x_train.shape)
    print(x_test.shape)
    

    我们将数组的高度和宽度相乘,以得到数组的总长度,但只在一个维度中: (28*28) = (784)。

  8. 现在我们准备将数据输入到模型中。我们将开始训练一个决策树:

    print ("Applying Decision Tree...")
    dtc = DecisionTreeClassifier()
    dtc.fit(x_train, y_train)
    

    为了查看该模型的表现,我们使用准确率作为度量指标。这表示已被预测的来自x_test的样本数,我们已经从metrics模块和 sklearn 导入了该模块。现在,我们将使用该模块中的accuracy_score()来计算模型的准确率。我们需要使用模型中的predict()函数预测来自x_test的结果,并查看输出是否与y_test标签匹配:

    y_pred = dtc.predict(x_test)
    accuracy = metrics.accuracy_score(y_test, y_pred)
    print(accuracy*100)
    

    之后,计算并打印准确率。得到的准确率为87.92%,对于决策树来说,这并不是一个坏的结果,但它还是可以改进的。

  9. 让我们尝试随机森林算法:

    print ("Applying RandomForest...")
    rfc = RandomForestClassifier(n_estimators=100)
    rfc.fit(x_train, y_train)
    

    使用相同的计算准确率的方法,得到的准确率是94.75%,这比之前的结果好多了,应该可以归类为一个好的模型。

  10. 现在,我们将尝试使用初始化为随机森林的 AdaBoost:

    print ("Applying Adaboost...")
    adaboost = AdaBoostClassifier(rfc,n_estimators=10)
    adaboost.fit(x_train, y_train)
    

    使用 AdaBoost 获得的准确率为95.67%。这个算法比之前的算法花费更多的时间,但得到了更好的结果。

  11. 现在我们将对上一个练习中获得的数字应用随机森林。我们选择这个算法是因为它比 AdaBoost 花费的时间要少得多,并且能提供更好的结果。在检查以下代码之前,你需要运行第一个练习中的代码,图像存储在Dataset/number.jpg文件夹中,这个图像是第一个练习使用的,还有从Dataset/testing/文件夹中提取的另外两张测试图像。完成这些后,你应该在你的目录中有五张数字图像,每张图像都可以加载。下面是代码:

    for number in range(5):
        imgLoaded = cv2.imread('number%d.jpg'%(number),0)
        img = cv2.resize(imgLoaded, (28, 28))
        img = img.flatten()
        img = img.reshape(1,-1)
        plt.subplot(1,5,number+1),
        plt.imshow(imgLoaded,'gray')
        plt.title(rfc.predict(img)[0])
        plt.xticks([]),plt.yticks([])
    plt.show()
    

图 2.28:随机森林对数字 1、6、2、1 和 6 的预测

图 2.28:随机森林对数字 1、6、2、1 和 6 的预测

在这里,我们应用了随机森林模型的predict()函数,将每个图像传递给它。随机森林似乎表现相当好,因为它正确预测了所有数字。让我们尝试另一个未使用过的数字(在Dataset文件夹内有一个文件夹包含一些测试图像):

图 2.29:随机森林对数字 1、5、8、3 和 4 的预测

图 2.29:随机森林对数字 1、5、8、3 和 4 的预测

它在其余数字上依然表现不错。让我们再尝试一个数字:

图 2.30:随机森林对数字 1、9、4、7 和 9 的预测

图 2.30:随机森林对数字 1、9、4、7 和 9 的预测

数字 7 似乎存在问题。这可能是因为我们没有引入足够的样本,并且模型的简单性也导致了问题。

注释

本次练习的完整代码可以在 GitHub 的 Lesson02 | Exercise07-09 文件夹中找到。

现在,在下一个主题中,我们将探索人工神经网络的世界,这些网络在完成这些任务时更为强大。

人工神经网络(ANNs)

人工神经网络ANNs)是模仿人脑并受其启发的信息处理系统,它们通过学习如何识别数据中的模式来模拟人脑。它们通过具有良好结构的架构来完成任务。该架构由多个小的处理单元(即神经元)组成,这些神经元通过相互连接来解决主要问题。

人工神经网络(ANNs)通过处理数据集中足够的示例来进行学习,足够的示例意味着成千上万,甚至是数百万个示例。这里的数据量可能成为一个劣势,因为如果你没有这些数据,你将不得不自己创建,这意味着你可能需要大量资金来收集足够的数据。

这些算法的另一个缺点是它们需要在特定的硬件和软件上进行训练。它们在高性能的 GPU 上训练效果最佳,而这些 GPU 价格昂贵。你仍然可以使用价格较低的 GPU 做某些事情,但数据训练的时间会更长。你还需要特定的软件,如TensorFlowKerasPyTorchFast.AI。对于本书,我们将使用 TensorFlow 和 Keras,它们运行在 TensorFlow 之上。

这些算法通过将所有数据作为输入来工作,其中第一层神经元作为输入层。之后,每个输入都会传递到下一层神经元,在那里它们会与某些值相乘,并通过激活函数进行处理,该函数做出“决策”并将这些值传递给下一层。网络中间的层被称为隐藏层。这个过程一直持续到最后一层,在那里输出结果。当将 MNIST 图像作为输入引入神经网络时,网络的最后一层应该有 10 个神经元,每个神经元代表一个数字,如果神经网络猜测某个图像是特定的数字,那么对应的神经元将被激活。人工神经网络检查其决策是否成功,如果没有,它会执行一个叫做反向传播的修正过程,在该过程中每次通过网络时都会被检查和修正,调整神经元的权重。图 2.31 展示了反向传播过程:

图 2.31:反向传播过程

图 2.31:反向传播过程

这是一个人工神经网络的图形表示:

图 2.32:ANN 架构

图 2.32:ANN 架构

在前面的图中,我们可以看到神经元,它们是所有处理发生的地方,以及它们之间的连接,它们是网络的权重。

我们将了解如何创建这些神经网络,但首先,我们需要查看我们所拥有的数据。

在前面的练习中,我们使用了形状为(60,072 和 784)以及(10,018 和 784)的整数类型,并且像素值为 0 到 255,分别用于训练和测试。人工神经网络(ANN)在使用归一化数据时表现得更好,速度也更快,但这到底是什么意思呢?

拥有归一化数据意味着将 0-255 范围的值转换为 0-1 的范围。这些值必须适应在 0 和 1 之间,这意味着它们将是浮动数字,因为没有其他方法可以将更大的数字范围压缩到较小的范围内。因此,首先我们需要将数据转换为浮动类型,然后进行归一化。以下是执行此操作的代码:

x_train = (x_train.astype(np.float32))/255.0 #Converts to float and then normalize
x_test = (x_test.astype(np.float32))/255.0 #Same for the test set
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

对于标签,我们也需要将格式转换为独热编码。

为此,我们需要使用 Keras 中 utils 包(现已更名为 np_utils)中的一个函数 to_categorical(),该函数将每个标签的数字转换为独热编码。以下是代码:

y_train = np_utils.to_categorical(y_train, 10)
y_test = np_utils.to_categorical(y_test, 10)

如果我们打印 y_train 的第一个标签 5,然后打印转换后的 y_train 的第一个值,它将输出 [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]。这种格式将在一个包含 10 个位置的数组的第六个位置放置 1(因为有 10 个数字),对应数字 5(第六个位置是为了 0,而不是 1)。现在我们可以继续进行神经网络的架构设计了。

对于一个基础的神经网络,使用了密集层(或全连接层)。这些神经网络也被称为全连接神经网络。它们包含一系列神经元,代表人类大脑的神经元。它们需要指定一个激活函数。激活函数是一个对输入进行加权求和、加上偏置并决定是否激活的函数(分别输出 1 或 0)。

最常用的激活函数是 Sigmoid 和 ReLU,但 ReLU 在整体上表现更好。它们在下图中表示:

图 2.33:Sigmoid 和 ReLU 函数

图 2.33:Sigmoid 和 ReLU 函数

Sigmoid 和 ReLU 函数计算加权和并添加偏置。然后它们根据该计算的值输出一个值。Sigmoid 函数会根据计算结果的值给出不同的值,范围从 0 到 1。而 ReLU 函数则对于负值输出 0,对于正值输出计算结果的值。

在神经网络的最后,通常会使用 softmax 激活函数,它将为每个类别输出一个非概率数值,该数值对于最可能与输入图像匹配的类别来说会更高。还有其他激活函数,但对于多分类问题,softmax 是最适合的输出函数。

Keras中,神经网络的代码如下:

model = Sequential()
model.add(Dense(16, input_shape=input_shape))
model.add(Activation('relu'))
model.add(Dense(8))
model.add(Activation('relu'))
model.add(Flatten())
model.add(Dense(10, activation="softmax"))

模型创建为 Sequential(),因为层是按顺序创建的。首先,我们添加一个包含 16 个神经元的密集层,并传递输入的形状,以便神经网络知道输入的形状。接着,应用 ReLU 激活函数。我们使用这个函数是因为它通常能给出很好的结果。然后,我们叠加另一个具有 8 个神经元且使用相同激活函数的层。

最后,我们使用 Flatten 函数将数组转换为一维,然后叠加最后一个密集层,其中类别数应表示神经元的数量(在这种情况下,MNIST 数据集有 10 个类别)。应用 softmax 函数,以便获得一热编码的结果,正如我们之前提到的。

现在我们需要编译模型。为此,我们使用如下的 compile 方法:

model.compile(loss='categorical_crossentropy', optimizer=Adadelta(), metrics=['accuracy'])

我们传入损失函数,用于计算反向传播过程中的误差。对于这个问题,我们将使用分类交叉熵作为损失函数,因为这是一个分类问题。使用的优化器是Adadelta,它在大多数情况下表现很好。我们将准确率作为模型的主要评价指标。

我们将使用在 Keras 中的回调函数。这些函数在每个 epoch 训练过程中都会被调用。我们将使用 Checkpoint 函数,以便在每个 epoch 上保存我们具有最佳验证结果的模型:

ckpt = ModelCheckpoint('model.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False)

用于训练这个模型的函数叫做 fit(),其实现如下:

model.fit(x_train, y_train, batch_size=64, epochs=10, verbose=1, validation_data=(x_test, y_test),callbacks=[ckpt])

我们传入训练集及其标签,并设置批次大小为 64(这些是每个 epoch 步骤中传递的图像),我们选择设置 10 次训练 epoch(每个 epoch 都会处理数据)。还传入验证集,以便查看模型在未见数据上的表现,最后,我们设置之前创建的回调函数。

所有这些参数必须根据我们面临的问题进行调整。为了将这一切付诸实践,我们将进行一个练习——这是我们在决策树中做过的相同练习,但这次使用的是神经网络。

练习 9:构建你的第一个神经网络

注意

我们将继续在这里编写练习 8 中的代码。

本练习的完整代码可以在 GitHub 的 Lesson02 | Exercise07-09 文件夹中找到。

  1. 前往你在 Google Colab 上执行 练习 8使用决策树、随机森林和 AdaBoost 算法预测数字 的界面。

  2. 现在从 Keras 库导入所需的包:

    from keras.callbacks import ModelCheckpoint
    from keras.layers import Dense, Flatten, Activation, BatchNormalization, Dropout
    from keras.models import Sequential
    from keras.optimizers import Adadelta
    from keras import utils as np_utils
    
  3. 我们按照本章中解释的方法对数据进行归一化处理。我们还声明了将传递给神经网络的input_shape实例,并打印出来:

    x_train = (x_train.astype(np.float32))/255.0
    x_test = (x_test.astype(np.float32))/255.0
    x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
    x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)
    y_train = np_utils.to_categorical(y_train, 10)
    y_test = np_utils.to_categorical(y_test, 10)
    input_shape = x_train.shape[1:]
    print(input_shape)
    print(x_train.shape)
    

    输出结果如下:

    图 2.34:通过神经网络归一化处理后的数据输出

    图 2.34:通过神经网络归一化处理后的数据输出
  4. 现在,我们将声明模型。我们之前构建的模型在这个问题上表现并不理想,所以我们创建了一个更深的模型,增加了更多神经元,并加入了一些新的方法:

    def DenseNN(input_shape):
        model = Sequential()
        model.add(Dense(512, input_shape=input_shape))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
        model.add(Dense(512))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
        model.add(Dense(256))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
        model.add(Flatten())
        model.add(Dense(256))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
        model.add(Dense(10, activation="softmax"))
    

    我们添加了一个BatchNormalization()方法,它帮助网络更快地收敛,并可能整体上获得更好的结果。

    我们还添加了Dropout()方法,它帮助网络避免过拟合(训练集的准确率远高于验证集的准确率)。它通过在训练过程中断开一些神经元(0.2 -> 20%的神经元),从而实现更好的问题泛化(更好地分类未见过的数据)。

    此外,神经元的数量大幅增加。层数也有所增加。随着层数和神经元的增加,理解会更深,学习到的特征也更复杂。

  5. 现在我们使用分类交叉熵来编译模型,因为有多个类别,并且使用 Adadelta,它在这些任务中表现非常好。同时,我们将准确率作为主要度量标准:

    model.compile(loss='categorical_crossentropy', optimizer=Adadelta(), metrics=['accuracy'])
    
  6. 让我们创建Checkpoint回调函数,其中模型将存储在Models文件夹中,文件名为model.h5。我们将使用验证损失作为主要的追踪方法,模型会被完整保存:

    ckpt = ModelCheckpoint('Models/model.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False)
    
  7. 开始使用fit()函数训练网络,就像我们之前解释的那样。我们使用 64 作为批次大小,10 个 epochs(足够了,因为每个 epoch 会持续很长时间,而且每个 epoch 之间的改善不会太大),并引入 Checkpoint 回调函数:

    model.fit(x_train, y_train, 
              batch_size=64,
              epochs=10,
              verbose=1,
              validation_data=(x_test, y_test),
              callbacks=[ckpt])
    

    这将花费一些时间。

    输出应该是这样的:

    图 2.35:神经网络输出

    图 2.35:神经网络输出

    模型的最终准确率对应于最后的val_acc,为97.83%。这个结果比我们使用 AdaBoost 或随机森林时获得的结果更好。

  8. 现在,让我们进行一些预测:

    for number in range(5):
        imgLoaded = cv2.imread('number%d.jpg'%(number),0)
        img = cv2.resize(imgLoaded, (28, 28))
        img = (img.astype(np.float32))/255.0
        img = img.reshape(1, 28, 28, 1)
        plt.subplot(1,5,number+1),plt.imshow(imgLoaded,'gray')
        plt.title(np.argmax(model.predict(img)[0]))
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    代码与上一练习中使用的代码相似,但有一些细微的不同。其中之一是,由于我们更改了输入格式,我们也需要更改输入图像的格式(浮动和归一化)。另一个是预测采用了 one-hot 编码,因此我们使用argmax()的 NumPy 函数来获取 one-hot 输出向量中最大值的位置,这将是预测的数字。

    让我们看看我们之前使用随机森林时尝试的最后一个数字的输出:

图 2.36:使用神经网络预测数字

图 2.36:使用神经网络预测数字

预测已成功——即使是随机森林模型困难的 7 也分类成功。

注意

完整代码可以在 GitHub 的 Lesson02 | Exercise07-09 文件夹中找到。

如果你尝试其他数字,它都会很好地分类——它已经学会了如何分类。

恭喜!你已经构建了你的第一个神经网络,并将其应用于现实世界的问题!现在你可以继续进行本章的活动了。

活动 2:从 Fashion-MNIST 数据库中分类 10 种衣物类型

现在你将面临一个与之前类似的问题,但这次涉及的是衣物类型的分类。这个数据库与原始 MNIST 非常相似,包含 60,000 张 28x28 的灰度图像用于训练,10,000 张用于测试。你需要按照第一个练习中的步骤进行,因为这个活动并不聚焦于现实世界。你将需要通过构建神经网络来实践在上一个练习中学到的能力。为此,你需要打开一个 Google Colab 笔记本。以下步骤将引导你朝着正确的方向前进:

  1. 从 Keras 加载数据集:

    from keras.datasets import fashion_mnist
    (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
    

    注意

    数据已像 MNIST 一样预处理,因此接下来的步骤应该类似于练习 5对图像应用各种形态学变换

  2. 导入random并设置种子为 42。导入matplotlib并绘制数据集中的五个随机样本,方法与上一个练习相同。

  3. 现在对数据进行归一化,并重新调整其形状,以便适配神经网络,并将标签转换为 one-hot 编码。

  4. 开始构建神经网络的架构,使用全连接层。你需要在一个方法中构建它,该方法将返回模型。

    注意

    我们建议从构建一个非常小且简单的架构开始,通过在给定数据集上进行测试来不断改进它。

  5. 使用合适的参数编译模型并开始训练神经网络。

  6. 一旦训练完成,我们应该进行一些预测以测试模型。我们已经将一些图像上传到上一个练习的Dataset文件夹中的testing文件夹。使用这些图像进行预测,方法与上一个练习中相同。

    注意

    你需要考虑到输入神经网络的图像背景是黑色的,而衣物是白色的,因此你应该做相应的调整,以使图像看起来像这些图像。如果需要,应该将白色和黑色反转。NumPy 有一个方法可以做到这一点:image = np.invert(image)

  7. 查看结果:

图 2.37:预测的输出是该列表中位置的索引

图 2.37:预测的输出是该列表中位置的索引

注意

此活动的解决方案可以在第 302 页找到。

总结

计算机视觉是人工智能中的一个重要领域。通过理解这个领域,你可以实现一些目标,例如从图像中提取信息,或生成看起来与现实生活中一模一样的图像。本章介绍了使用 OpenCV 库进行图像预处理和特征提取的方法,借此可以轻松地训练和预测机器学习模型。还介绍了一些基础的机器学习模型,如决策树和提升算法。这些内容作为机器学习的入门,主要用于实验和玩耍。最后,介绍了神经网络,并使用 Keras 和 TensorFlow 作为后端进行编码。讲解了归一化并进行了实践操作,还涉及了全连接层,尽管卷积层比全连接层更适合处理图像,卷积层将在书的后续章节中讲解。

还介绍了避免过拟合的概念,最后我们使用该模型进行了预测,并通过真实世界的图像进行了实践操作。

在下一章,将介绍自然语言处理NLP)的基本概念,并展示一些最广泛使用的技术,这些技术用于从语料库中提取信息,以便创建语言预测的基本模型。

第四章:第三章

自然语言处理基础

学习目标

本章结束时,你将能够:

  • 分类自然语言处理的不同领域

  • 分析 Python 中的基本自然语言处理库

  • 预测一组文本中的主题

  • 开发一个简单的语言模型

本章涵盖了自然语言处理的不同基础知识和领域,并介绍了 Python 中的相关库。

介绍

自然语言处理NLP)是人工智能AI)的一个领域,旨在使计算机能够理解并处理人类语言,以执行有用的任务。在这个领域中,有两个部分:自然语言理解NLU)和自然语言生成NLG)。

近年来,人工智能改变了机器与人类的互动方式。人工智能通过执行一些任务帮助人们解决复杂的方程式,比如根据你的口味推荐电影(推荐系统)。得益于 GPU 的高性能和海量数据的可用性,已经可以创建能够学习并表现得像人类一样的智能系统。

有许多库旨在帮助创建这些系统。在本章中,我们将回顾一些最著名的 Python 库,用于从原始文本中提取和清理信息。你可能会认为这项任务很复杂,但语言的完整理解和解释本身就是一项艰巨的任务。例如,“Cristiano Ronaldo 进了三个球”这句话对于机器来说很难理解,因为它不知道 Cristiano Ronaldo 是谁,也不知道“进了三个球”是什么意思。

自然语言处理(NLP)中最受欢迎的主题之一是问答系统QA)。这个领域还包含信息检索IR)。这些系统通过查询数据库中的知识或信息来构建答案,但它们也能够从一组自然语言文档中提取答案。这就是像谷歌这样的搜索引擎的工作原理。

目前在行业中,自然语言处理正变得越来越流行。最新的 NLP 趋势包括在线广告匹配、情感分析、自动翻译和聊天机器人。

对话代理,通常被称为聊天机器人,是自然语言处理的下一个挑战。它们能够进行真实的对话,许多公司使用它们来获取产品反馈或创建新的广告活动,通过分析客户在聊天机器人中的行为和意见。虚拟助手是自然语言处理的一个极好例子,它们已经进入市场。最著名的包括 Siri、亚马逊的 Alexa 和 Google Home。在本书中,我们将创建一个聊天机器人来控制一个虚拟机器人,使其能够理解我们希望机器人做什么。

自然语言处理

如前所述,NLP 是一个涉及理解和处理人类语言的人工智能领域。NLP 位于人工智能、计算机科学和语言学的交汇点。这个领域的主要目标是让计算机理解用人类语言写成的陈述或词语:

图 3.1:人工智能、语言学和计算机科学中的自然语言处理表示

图 3.1:人工智能、语言学和计算机科学中的自然语言处理表示

语言学科学专注于研究人类语言,试图描述和解释语言的不同方法。

语言可以被定义为一组规则和符号的集合。符号通过规则组合使用,以传达信息。人类语言是特别的。我们不能简单地把它看作是自然形成的符号和规则;根据上下文,词汇的意义可能会发生变化。

自然语言处理正变得越来越流行,并且可以解决许多困难的问题。可用的文本数据量非常大,人类无法处理所有这些数据。在维基百科中,每天平均有 547 篇新文章,总共有超过 500 万篇文章。可以想象,人类无法阅读所有这些信息。

NLP 面临三个挑战。第一个挑战是收集所有数据,第二个是对数据进行分类,最后一个是提取相关信息。

自然语言处理解决了许多繁琐的任务,如电子邮件中的垃圾邮件检测、词性POS)标注和命名实体识别。通过深度学习,NLP 还可以解决语音转文本问题。尽管 NLP 展示了很强的能力,但也存在一些情况,例如在人机对话中没有得到良好的解决方案、问答系统的摘要和机器翻译等。

自然语言处理的部分

如前所述,自然语言处理(NLP)可以分为两个部分:自然语言理解(NLU)和自然语言生成(NLG)。

自然语言理解

本节的自然语言处理涉及对人类语言的理解和分析。它侧重于对文本数据的理解,并处理这些数据以提取相关信息。NLU 提供直接的人机交互,并执行与语言理解相关的任务。

NLU 涉及人工智能最具挑战性的任务之一,那就是文本的理解。NLU 的主要挑战是理解对话。

注意

自然语言处理使用一套方法来生成、处理和理解语言。自然语言理解通过功能来理解文本的意义。

以前,对话被表示为一棵树,但这种方法无法涵盖许多对话情况。为了覆盖更多的情况,需要更多的树,每个对话上下文对应一棵树,这就导致了许多句子的重复:

图 3.2:使用树表示对话

图 3.2:使用树表示对话

这种方法已经过时且低效,因为它基于固定的规则;本质上是一个 if-else 结构。但现在,NLU(自然语言理解)贡献了另一种方法。对话可以表示为一个维恩图,其中每个集合代表对话的一个上下文:

图 3.3:使用维恩图表示对话

图 3.3:使用维恩图表示对话

如你在之前的图中所见,NLU 方法通过改进对话理解的结构,提升了效果,因为它不是一个包含 if-else 条件的固定结构。NLU 的主要目标是解释人类语言的意义,处理对话的上下文,解决歧义并管理数据。

自然语言生成

NLG 是生成具有意义和结构的短语、句子和段落的过程。它是 NLP 的一个领域,不涉及理解文本。

要生成自然语言,NLG 方法需要相关数据。

NLG 有三个组成部分:

  • 生成器:负责将文本包含在意图中,使其与情境的上下文相关联。

  • 组成部分和表示层次:为生成的文本提供结构

  • 应用:保存对话中的相关数据,以跟随逻辑脉络

生成的文本必须是人类可读的格式。NLG 的优点在于你可以使数据变得易于访问,并且可以迅速创建报告摘要。

自然语言处理的层次

人类语言有不同的表示层次。每个表示层次都比前一个层次更复杂。随着层次的上升,理解语言变得越来越困难。

前两个层次依赖于数据类型(音频或文本),具体分为以下几类:

  • 语音学分析:如果数据是语音,首先我们需要分析音频以获得句子。

  • OCR/分词:如果我们有文本,需要通过计算机视觉(OCR)识别字符并构成单词。如果没有,我们需要对文本进行分词(即将句子拆分成文本单元)。

    注意

    OCR 过程是识别图像中的字符。一旦它生成了单词,这些单词就会作为原始文本进行处理。

  • 形态学分析:专注于句子的单词,并分析其语素。

  • 句法分析:这一层次专注于句子的语法结构。即理解句子中的不同部分,比如主语或谓语。

  • 语义表示:程序并不理解单个词汇;它可以通过知道单词在句子中的使用方式来理解词汇的意义。例如,“猫”和“狗”对于算法来说可能意味着相同,因为它们可以以相同的方式使用。通过这种方式理解句子被称为词汇级别的意义。

  • 话语处理:分析和识别文本中连接的句子及其关系。通过这样做,算法可以理解文本的主题是什么。

自然语言处理(NLP)在今天的行业中展现出巨大的潜力,但也存在一些例外。通过使用深度学习的概念,我们可以处理这些例外问题,从而获得更好的结果。这些问题将在第四章神经网络与 NLP中进行回顾。文本处理技术的优势和递归神经网络的改进是 NLP 变得越来越重要的原因。

Python 中的 NLP

近年来,Python 变得非常流行,它将通用编程语言的强大功能与特定领域语言的使用相结合,如 MATLAB 和 R(用于数学和统计)。它有不同的库用于数据加载、可视化、NLP、图像处理、统计等。Python 拥有最强大的文本处理和机器学习算法库。

自然语言工具包(NLTK)

NLTK 是 Python 中用于处理人类语言数据的最常见工具包。它包括一套用于处理自然语言和统计数据的库和程序。NLTK 通常作为学习工具和进行研究时使用。

该库提供了超过 50 个语料库和词汇资源的接口和方法。NLTK 能够对文本进行分类并执行其他功能,如分词、词干提取(提取单词的词干)、标注(识别单词的标签,如人名、城市等)和句法分析。

练习 10:NLTK 入门

在本练习中,我们将回顾关于 NLTK 库的最基本概念。正如我们之前所说,这个库是自然语言处理(NLP)领域中最广泛使用的工具之一。它可以用来分析和研究文本,忽略无关的信息。这些技术可以应用于任何文本数据,例如,从一组推文中提取最重要的关键词,或分析一篇报纸文章:

注意

本章中的所有练习将在 Google Colab 中执行。

  1. 打开你的 Google Colab 界面。

  2. 为书籍创建一个文件夹。

  3. 在这里,我们将使用 NLTK 库的基本方法处理一个句子。首先,让我们导入必要的方法(stopwordsword_tokenizesent_tokenize):

    from nltk.corpus import stopwords
    from nltk.tokenize import word_tokenize
    from nltk.tokenize import sent_tokenize 
    import nltk
    nltk.download('punkt')
    
  4. 现在我们创建一个句子并应用这些方法:

    example_sentence = "This course is great. I'm going to learn deep learning; Artificial Intelligence is amazing and I love robotics..."
    sent_tokenize(example_sentence) # Divide the text into sentences
    

    图 3.4:将句子划分为子句
    word_tokenize(example_sentence)
    

    图 3.5:将句子分解成单词

    图 3.5:将句子分解成单词

    注意

    Sent_tokenize 返回一个包含不同句子的列表。NLTK 的一个缺点是 sent_tokenize 并没有分析整个文本的语义结构;它只是根据句号将文本分割。

  5. 通过单词分词后的句子,我们来去除停用词。停用词是一组没有关于文本相关信息的词语。在使用停用词之前,我们需要下载它:

    nltk.download('stopwords')
    
  6. 现在,我们将停用词的语言设置为英语:

    stop_words = set(stopwords.words("english")) 
    print(stop_words)
    

    输出如下:

    图 3.6: 停用词设置为英语

    图 3.6: 停用词设置为英语
  7. 处理句子,删除停用词

    print(word_tokenize(example_sentence))
    print([w for w in word_tokenize(example_sentence.lower()) if w not in stop_words]) 
    

    输出如下:

    图 3.7: 去除停用词后的句子

    图 3.7: 去除停用词后的句子
  8. 现在,我们可以修改停用词的集合并检查输出:

    stop_words = stop_words - set(('this', 'i', 'and')) 
    print([w for w in word_tokenize(example_sentence.lower()) if w not in stop_words]) 
    

    图 3.8: 设置停用词

    图 3.8: 设置停用词
  9. 词干提取器去除单词的形态学词缀。让我们定义一个词干提取器并处理我们的句子。Porter 词干提取器是一种执行此任务的算法:

    from nltk.stem.porter import *    # importing a stemmer
    stemmer = PorterStemmer()    # importing a stemmer     
    print([stemmer.stem(w) for w in  word_tokenize(example_sentence)])
    

    输出如下:

    图 3.9: 设置停用词

    图 3.9: 设置停用词
  10. 最后,让我们按类型对每个单词进行分类。为此,我们将使用一个词性标注器:

    nltk.download('averaged_perceptron_tagger')
    t = nltk.pos_tag(word_tokenize(example_sentence)) #words with each tag
    t
    

    输出如下:

图 3.10: 词性标注器

图 3.10: 词性标注器

注意

平均感知机标注器是一种算法,用于预测单词的类别。

正如你在这次练习中可能已经注意到的,NLTK 能够轻松处理一个句子。它还可以分析大量文本文档,毫无问题。它支持多种语言,且分词过程比类似的库要快,并且每个 NLP 问题都有许多方法可供使用。

spaCy

spaCy 是 Python 中的另一个自然语言处理库。它看起来与 NLTK 相似,但你会发现它的工作方式有所不同。

spaCy 由 Matt Honnibal 开发,旨在帮助数据科学家轻松清理和标准化文本。它是准备机器学习模型文本数据的最快库。它包含内置的词向量和一些方法,用于比较两个或多个文本之间的相似性(这些方法是通过神经网络训练的)。

它的 API 易于使用,比 NLTK 更直观。通常,在自然语言处理(NLP)中,spaCy 与 NumPy 进行比较。它提供了执行分词、词形还原、词性标注、命名实体识别(NER)、依赖解析、句子和文档相似性分析、文本分类等任务的方法和功能。

它不仅具有语言学特征,还拥有统计模型。这意味着你可以预测一些语言注释,例如判断一个词是动词还是名词。根据你希望进行预测的语言,你需要更改一个模块。在这一部分中有 Word2Vec 模型,我们将在第四章中讨论,神经网络与自然语言处理

正如我们之前所说,spaCy 有许多优点,但也有一些缺点;例如,它仅支持 8 种语言(NLTK 支持 17 种语言),分词过程较慢(这个耗时的过程在长文本中可能会很关键),而且总的来说,它不够灵活(也就是说,它只提供了 API 方法,无法修改任何参数)。

在开始练习之前,我们先回顾一下 spaCy 的架构。spaCy 最重要的数据结构是 Doc 和 Vocab。

Doc 结构是你正在加载的文本;它不是一个字符串。它由一系列标记及其注释组成。Vocab 结构是一组查找表,那么什么是查找表,它为什么重要呢?查找表是计算中的一个数组索引操作,它替代了运行时操作。spaCy 将跨文档可用的信息集中化。这意味着它更加高效,因为这样节省了内存。没有这些结构,spaCy 的计算速度将会更慢。

然而,Doc 的结构与 Vocab 不同,因为 Doc 是数据的容器。一个 Doc 对象拥有数据,并由一系列标记或跨度组成。还有一些词素(lexemes),它们与 Vocab 结构有关,因为它们没有上下文(与标记容器不同)。

注意

词素是没有屈折词尾的词汇意义单位。研究这一领域的是形态学分析。

图 3.11 显示了 spaCy 架构。

图 3.11: spaCy 架构

图 3.11:spaCy 架构

根据你加载的语言模型不同,你将拥有不同的处理流程和 Vocab。

练习 11:spaCy 简介

在这个练习中,我们将进行与练习 10NLTK 简介中相同的转换,使用 spaCy API 对该练习中的同一个句子进行操作。这个练习将帮助你理解和学习这些库之间的差异:

  1. 打开你的 Google Colab 界面。

  2. 为这本书创建一个文件夹。

  3. 然后,导入包以使用它的所有功能:

    import spacy
    
  4. 现在我们将初始化我们的nlp对象。这个对象是 spaCy 方法的一部分。通过执行这行代码,我们正在加载括号内的模型:

    import en_core_web_sm
    nlp = spacy.load('en')
    
  5. 我们使用与练习 10NLTK 简介中相同的句子,并创建 Doc 容器:

    example_sentence = "This course is great. I'm going to learn deep learning; Artificial Intelligence is amazing and I love robotics..."
    doc1 = nlp(example_sentence)
    
  6. 现在,打印doc1、它的格式,第 5 个和第 11 个标记,以及第 5 个和第 11 个标记之间的跨度。你将看到如下结果:

    print("Doc structure: {}".format(doc1))
    print("Type of doc1:{}".format(type(doc1)))
    print("5th and 10th Token of the Doc: {}, {}".format(doc1[5], doc1[11]))
    print("Span between the 5th token and the 10th: {}".format(doc1[5:11]))
    

    输出结果如下:

    图 3.12: spaCy 文档输出

    图 3.12:spaCy 文档输出
  7. 正如我们在图 3.5 中看到的,文档由标记(tokens)和跨度(spans)组成。首先,我们将看到doc1的跨度,然后是它的标记。

    打印跨度:

    for s in doc1.sents:
        print(s)
    

    输出结果如下:

    图 3.13: 打印 doc1 的跨度

    图 3.13:打印 doc1 的跨度

    打印令牌:

    for i in doc1:
        print(i)
    

    输出结果如下:

    图 3.14:打印 doc1 的令牌
  8. 一旦我们将文档划分为令牌,停用词就可以被去除。

    首先,我们需要导入它们:

    from spacy.lang.en.stop_words import STOP_WORDS
    print("Some stopwords of spaCy: {}".format(list(STOP_WORDS)[:10]))
    type(STOP_WORDS)
    

    输出结果如下:

    图 3.15:spaCy 中的 10 个停用词

    图 3.15:spaCy 中的 10 个停用词

    但令牌容器有 is_stop 属性:

    for i in doc1[0:5]:
        print("Token: {} | Stop word: {}".format(i, i.is_stop)
    

    输出结果如下:

    图 3.16:令牌的  属性

    ](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/art-vis-lang-proc-rbt/img/C13550_03_16.jpg)

    图 3.16:令牌的 is_stop 属性
  9. 要添加新的停用词,我们必须修改 vocab 容器:

    nlp.vocab["This"].is_stop = True doc1[0].is_stop
    

    这里的输出结果如下:

  10. 要执行词性标注,我们初始化令牌容器:

    for i in doc1[0:5]:
        print("Token: {} | Tag: {}".format(i.text, i.pos_))
    

    输出结果如下:

    图 3.17:令牌的  属性

    图 3.17:令牌的 .pos_ 属性
  11. 文档容器具有 ents 属性,包含令牌的实体。为了在文档中包含更多实体,我们可以声明一个新的实体:

    doc2 = nlp("I live in Madrid and I am working in Google from 10th of December.")
    for i in doc2.ents:
        print("Word: {} | Entity: {}".format(i.text, i.label_))
    

    输出结果如下:

图 3.18:令牌的  属性

图 3.18:令牌的 .label_ 属性

注意

如你在本练习中所见,spaCy 比 NLTK 更易于使用,但 NLTK 提供了更多方法来执行不同的文本操作。spaCy 非常适合用于生产环境。这意味着,在最短的时间内,你就能对文本进行基本处理。

练习已结束!现在你可以使用 NLTK 或 spaCy 对文本进行预处理。根据你要执行的任务,你将能够选择其中一个库来清理数据。

主题建模

在自然语言理解(NLU)中,作为自然语言处理(NLP)的一部分,许多任务之一是提取句子、段落或整个文档的含义。理解文档的一种方法是通过其主题。例如,如果一组文档来自一份报纸,那么这些主题可能是政治或体育。通过主题建模技术,我们可以获得一组代表不同主题的词语。根据你的文档集,你将拥有由不同词语代表的不同主题。这些技术的目标是了解语料库中不同类型的文档。

术语频率 – 逆文档频率(TF-IDF)

TF-IDF 是一种常用的 NLP 模型,用于从文档中提取最重要的词汇。为了进行这种分类,算法会为每个单词分配一个权重。这种方法的思想是忽略那些与全球概念(即文本的整体主题)无关的词语,因此这些词语的权重会被降低(意味着它们将被忽略)。降低它们的权重将帮助我们找到该文档的关键字(即权重最大的词语)。

数学上,算法用来查找文档中术语权重的方法如下:

图 3.19:TF-IDF 公式

图 3.19:TF-IDF 公式
  • Wi,j:术语 i 在文档 j 中的权重

  • tf,j: i 在 j 中的出现次数

  • df,j: 包含 i 的文档数量

  • N: 文档的总数

结果是术语在该文档中出现的次数,乘以总文档数的对数,再除以包含该术语的文档数量。

潜在语义分析(LSA)

LSA 是主题建模的基础技术之一。它分析文档集与其术语之间的关系,并生成与之相关的一组概念。

与 TF-IDF 相比,LSA 更具前瞻性。在大规模文档集中,TF-IDF 矩阵包含大量噪声信息和冗余维度,因此 LSA 算法执行了降维处理。

这一降维处理通过奇异值分解(SVD)进行。SVD 将矩阵 M 分解为三个独立矩阵的乘积:

图 3.20:奇异值分解

图 3.20:奇异值分解
  • A: 这是输入数据矩阵。

  • m: 这是文档的数量。

  • n: 这是术语的数量。

  • U: 左奇异向量。我们的文档-主题矩阵。

  • S: 奇异值。表示每个概念的强度。这是一个对角矩阵。

  • V: 右奇异向量。表示术语在主题中的向量。

    注意

    该方法在大规模文档集上更为高效,但还有更好的算法可以执行此任务,比如 LDA 或 PLSA。

练习 12:Python 中的主题建模

在本练习中,将使用特定的库在 Python 中编写 TF-IDF 和 LSA 代码。完成本练习后,你将能够执行这些技术来提取文档中术语的权重:

  1. 打开你的 Google Colab 界面。

  2. 为书籍创建一个文件夹。

  3. 为了生成 TF-IDF 矩阵,我们可以编写图 3.19 中的公式,但我们将使用 Python 中最著名的机器学习库之一——scikit-learn:

    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.decomposition import TruncatedSVD
    
  4. 我们将在本练习中使用的语料库非常简单,只有四个句子:

    corpus = [
         'My cat is white',
         'I am the major of this city',
         'I love eating toasted cheese',
         'The lazy cat is sleeping',
    ]
    
  5. 使用TfidfVectorizer方法,我们可以将语料库中的文档集合转换为 TF-IDF 特征矩阵:

    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(corpus)
    
  6. get_feature_names()方法显示提取的特征。

    注意

    vectorizer.get_feature_names()
    

    输出结果如下:

    图 3.21:语料库的特征名称

    图 3.21:语料库的特征名称
  7. X 是一个稀疏矩阵。要查看其内容,我们可以使用todense()函数:

    X.todense()
    

    输出结果如下:

    图 3.22:语料库的 TF-IDF 矩阵

    图 3.22:语料库的 TF-IDF 矩阵
  8. 现在让我们使用 LSA 进行降维。TruncatedSVD方法使用 SVD 对输入矩阵进行变换。在本练习中,我们将使用n_components=10。从现在开始,你需要使用n_components=100(它在较大的语料库中有更好的效果):

    lsa = TruncatedSVD(n_components=10,algorithm='randomized',n_iter=10,random_state=0)
    lsa.fit_transform(X)
    

    输出结果如下:

    图 23:使用 LSA 进行降维

    图 23:使用 LSA 进行降维
  9. attribute .components_ 显示每个 vectorizer.get_feature_names() 的权重。注意,LSA 矩阵的范围为 4x16,我们的语料库中有 4 个文档(概念),而矢量化器有 16 个特征(术语):

    lsa.components_
    

    输出如下:

图 3.24:期望的 TF-IDF 矩阵输出

](https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/art-vis-lang-proc-rbt/img/C13550_03_24.jpg)

图 3.24:期望的 TF-IDF 矩阵输出

练习已经成功结束!这是活动 3的预备练习,处理语料库。请务必查看练习的第七步——它将为你提供完成后续活动的关键。我鼓励你阅读 scikit-learn 文档,学习如何发现这两种方法的潜力。现在你已经知道如何创建 TF-IDF 矩阵。这个矩阵可能会非常庞大,因此,为了更好地管理数据,LSA 算法对文档中每个术语的权重进行了降维处理。

活动 3:处理语料库

在本活动中,我们将处理一个非常小的语料库,通过 LSA 清理数据并提取关键词和概念。

想象一下这个场景:你所在城镇的报摊举办了一场比赛。比赛的内容是预测一篇文章的类别。该报纸没有结构化数据库,这意味着它只有原始数据。他们提供了一小组文档,需要知道这篇文章是政治类、科学类还是体育类:

注意

你可以在 spaCy 和 NLTK 库之间选择进行活动。如果在 LSA 算法结束时,关键词相关性得以保留,那么两种解决方案都有效。

  1. 加载语料库文档并将其存储在列表中。

    注意

    语料库文档可以在 GitHub 上找到,github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/tree/master/Lesson03/Activity03/dataset

  2. 使用 spaCy 或 NLTK 预处理文本。

  3. 应用 LSA 算法。

  4. 显示与每个概念相关的前五个关键词:

    关键词:moon, apollo, earth, space, nasa

    关键词:yard, touchdown, cowboys, prescott, left

    关键词:facebook, privacy, tech, consumer, data

    注意

    输出的关键词可能与你的不一样。如果你的关键词不相关,请检查解决方案。

    输出如下:

图 3.25:概念中最相关词语的输出示例(f1)

图 3.25:概念中最相关词语的输出示例(f1)

注意

该活动的解决方案可以在第 306 页找到。

语言建模

到目前为止,我们已经回顾了文本数据预处理的最基本技术。现在,我们将深入探讨自然语言的结构——语言模型。我们可以将此话题视为自然语言处理(NLP)中机器学习的入门。

语言模型简介

一个统计语言模型LM)是一个单词序列的概率分布,这意味着,它为一个特定的句子分配一个概率。例如,语言模型可以用来计算句子中即将到来的单词的概率。这涉及到对语言模型结构以及如何形成它做出一些假设。一个语言模型的输出从来不是完全正确的,但使用它通常是必要的。

语言模型(LM)在许多 NLP 任务中都有应用。例如,在机器翻译中,了解下一句前面的句子非常重要。语言模型还用于语音识别,以避免歧义,拼写纠错以及摘要生成等。

让我们看看语言模型是如何在数学上表示的:

  • P(W) = P(w1, w2, w3, w4, … wn)

P(W) 是我们的语言模型(LM),wi 是包含在 W 中的单词,正如我们之前提到的,我们可以用它来计算即将到来的单词的概率,方式如下:

  • P(w5|w1, w2, w3, w4)

这个(w1, w2, w3, w4)表示在给定的单词序列中,w5(即将到来的单词)的概率可能是多少。

看这个例子,P(w5|w1, w2, w3, w4),我们可以做出这样的假设:

  • P(实际单词 | 前一个单词)

根据我们查看的前几个单词的数量来获取实际单词的概率,我们可以使用不同的模型。那么,现在我们将介绍一些关于这些模型的重要概念。

Bigram 模型

bigram 模型是由两个连续的单词组成的序列。例如,在句子“我的猫是白色的”中,有以下这些 bigram:

我的猫

猫是

是白色的

从数学上讲,bigram 模型有以下形式:

  • Bigram 模型:P(wi|wi-1)

N-gram 模型

如果我们改变前一个单词的长度,就得到了 N-gram 模型。它的工作原理与 bigram 模型相似,但考虑的单词比前一个集合更多。

使用之前的例子“我的猫是白色的”,我们可以得到以下结果:

  • Trigram

    我的猫是

    猫是白色的

  • 4-gram

  • 我的猫是白色的

N-Gram 问题

此时,你可能会认为 n-gram 模型比 bigram 模型更准确,因为 n-gram 模型可以访问更多的“先前知识”。然而,由于长距离依赖,n-gram 模型也存在一定的局限性。一个例子是,“经过深思熟虑,我买了一台电视”,我们将其计算为:

  • P(电视 | 经过深思熟虑,我买了一台)

句子“经过深思熟虑,我买了一台电视”可能是我们语料库中唯一具有这种结构的单词序列。如果我们将“电视”这个词换成另一个词,例如“电脑”,句子“经过深思熟虑,我买了一台电脑”也是有效的,但在我们的模型中,以下情况将会发生:

  • P(电脑 | 经过深思熟虑,我买了一台) = 0

这个句子是有效的,但我们的模型不够准确,所以我们在使用 n-gram 模型时需要小心。

计算概率

Unigram 概率

单语是计算概率的最简单情况。它计算一个词在一组文档中出现的次数。它的公式如下:

图 3.26:单语概率估计

图 3.27:单语概率估计
  • c(wi) 是出现次数

  • wi 在整个语料库中出现。语料库的大小就是它包含的词项数量。

双语概率

为了估计双语概率,我们将使用最大似然估计:

图 3.27:双语概率估计

图 3.27:双语概率估计

为了更好地理解这个公式,我们来看一个例子。

假设我们的语料库由这三句话组成:

我的名字是查尔斯。

查尔斯是我的名字。

我的狗在玩球。

语料库的大小是 14 个词,现在我们要估计 "my name" 这一序列的概率。

图 3.28:双语估计的例子

图 3.28:双语估计的例子

链式法则

现在我们了解了双语和 n-gram 的概念,我们需要了解如何获得这些概率。

如果你有基本的统计学知识,你可能会认为最好的选择是应用链式法则,将每个概率连接起来。例如,在句子 "My cat is white" 中,概率如下:

  • P(my cat is white) = p(white|my cat is) p(is|my cat) p(cat|my) p(my)

这似乎在这个句子中是可行的,但如果我们有一个更长的句子,长距离依赖问题就会出现,n-gram 模型的结果可能会不正确。

平滑

到目前为止,我们有了一个概率模型,如果我们想估计模型的参数,可以使用最大似然估计法。

语言模型(LM)面临的一个大问题是数据不足。我们的数据是有限的,因此会有许多未知事件。这意味着什么?这意味着我们最终得到的语言模型会对未见过的词汇给出 0 的概率。

为了解决这个问题,我们将使用平滑方法。通过这个平滑方法,每个概率估计结果都会大于零。我们将使用的方法是加一平滑:

图 3.29:双语估计中的加一平滑

图 3.29:双语估计中的加一平滑

V 是我们语料库中不同词项的数量。

注意

还有更多表现更好的平滑方法;这只是最基本的方法。

马尔可夫假设

马尔可夫假设对于估计长句子的概率非常有用。通过这种方法,我们可以解决长距离依赖问题。马尔可夫假设简化了链式法则,用于估计长序列的词汇。每次估计只依赖于前一步:

图 3.30:马尔可夫假设

图 3.30:马尔可夫假设

我们也可以使用二阶马尔可夫假设,它依赖于前两个词项,但我们将使用一阶马尔可夫假设:

图 3.31:马尔科夫示例

图 3.31:马尔科夫示例

如果我们将其应用于整个句子,结果如下:

图 3.32:整个句子的马尔科夫示例

图 3.32:整个句子的马尔科夫示例

按照上述方式分解单词序列将更加准确地输出概率。

练习 13:创建二元模型

在这个练习中,我们将创建一个简单的语言模型(LM),使用 unigram 和 bigram。同样,我们将比较在没有加一平滑和加一平滑的情况下创建语言模型的结果。n-gram 的一个应用示例是键盘应用。它们可以预测你下一个单词。这个预测可以通过一个二元模型来实现:

  1. 打开你的 Google Colab 界面。

  2. 创建书籍文件夹。

  3. 声明一个小的、易于训练的语料库:

    import numpy as np
    corpus = [
         'My cat is white',
         'I am the major of this city',
         'I love eating toasted cheese',
         'The lazy cat is sleeping',
    ]
    
  4. 导入所需的库并加载模型:

    import spacy
    import en_core_web_sm
    from spacy.lang.en.stop_words import STOP_WORDS
    nlp = en_core_web_sm.load()
    
  5. 使用 spaCy 对其进行分词。为了加快平滑处理和二元模型的速度,我们将创建三个列表:

    Tokens:语料库中的所有标记

    Tokens_doc:包含每个语料库标记的列表的列表

    Distinc_tokens:去重后的所有标记:

    tokens = []
    tokens_doc = []
    distinc_tokens = []
    

    我们先创建一个循环,遍历语料库中的句子。doc 变量将包含句子的标记序列:

    for c in corpus:
        doc = nlp(c)
        tokens_aux = []
    

    现在,我们将创建第二个循环,遍历标记并将其推入相应的列表中。t 变量将是句子的每个标记:

        for t in doc:
            tokens_aux.append(t.text)
            if t.text not in tokens:
                distinc_tokens.append(t.text) # without duplicates 
            tokens.append(t.text)
        tokens_doc.append(tokens_aux)
        tokens_aux = []
        print(tokens)
        print(distinc_tokens)
        print(tokens_doc)
    
  6. 创建 unigram 模型并进行测试:

    def unigram_model(word):
        return tokens.count(word)/len(tokens)
    unigram_model("cat")
    

    结果 = 0.1388888888888889

  7. 添加平滑并使用相同的单词进行测试:

    def unigram_model_smoothing(word):
        return (tokens.count(word) + 1)/(len(tokens) + len(distinc_tokens))
    unigram_model_smoothing("cat")
    

    结果 = 0.1111111111111111

    注意

    这种平滑方法的问题在于每个未见过的单词都有相同的概率。

  8. 创建二元模型(bigram):

    def bigram_model(word1, word2):
        hit = 0
    
  9. 我们需要遍历文档中的所有标记,尝试找到 word1word2 一起出现的次数:

        for d in tokens_doc:
            for t,i in zip(d, range(len(d))): # i is the length of d  
                if i <= len(d)-2:
                    if word1 == d[i] and word2 == d[i+1]:
                        hit += 1
        print("Hits: ",hit)
        return hit/tokens.count(word1)
    bigram_model("I","am")
    

    输出如下:

    图 3.33:输出显示 word1 和 word2 在文档中一起出现的次数

    图 3.33:输出显示 word1 和 word2 在文档中一起出现的次数
  10. 为二元模型添加平滑:

    def bigram_model_smoothing(word1, word2):
        hit = 0
        for d in tokens_doc:
            for t,i in zip(d, range(len(d))):
                if i <= len(d)-2:
                    if word1 == d[i] and word2 == d[i+1]:
                        hit += 1
        return (hit+1)/(tokens.count(word1)+len(distinc_tokens))
    bigram_model("I","am")
    

    输出如下:

图 3.34:为模型添加平滑后输出结果

图 3.34:为模型添加平滑后输出结果

恭喜!你已经完成了本章的最后一个练习。在下一章,你将看到这种语言模型(LM)方法是一种基础的深度自然语言处理(NLP)方法。现在你可以利用庞大的语料库,创建属于你自己的语言模型(LM)。

注意

应用马尔科夫假设,最终的概率将四舍五入为 0。我建议使用 log()并逐个添加各个组件。此外,检查代码的精度位数(float16 < float32 < float64)。

总结

自然语言处理(NLP)在人工智能中变得越来越重要。各个行业分析大量未经结构化的原始文本数据。为了理解这些数据,我们使用许多库进行处理。NLP 分为两大类方法和功能:NLG 用于生成自然语言,NLU 用于理解自然语言。

首先,清理文本数据非常重要,因为其中会有很多无用的、不相关的信息。一旦数据准备好进行处理,通过诸如 TF-IDF 或 LSA 等数学算法,就能理解大量文档。像 NLTK 和 spaCy 这样的库在完成这项任务时非常有用,它们提供了去除数据噪音的方法。文档可以被表示为一个矩阵。首先,TF-IDF 能够给出文档的全局表示,但当语料库较大时,更好的选择是通过 LSA 和 SVD 进行降维处理。scikit-learn 提供了处理文档的算法,但如果文档没有经过预处理,结果将不准确。最后,可能需要使用语言模型,但它们需要由有效的训练集文档构成。如果文档集质量良好,语言模型应该能够生成语言。

在下一章中,我们将介绍递归神经网络RNNs)。我们将探讨这些 RNN 的一些高级模型,并因此在构建我们的机器人时走在前列。

第五章:第四章

带有自然语言处理的神经网络

学习目标

到本章结束时,你将能够:

  • 解释什么是循环神经网络

  • 设计和构建循环神经网络

  • 评估非数值数据

  • 评估使用 RNN 的不同最先进的语言模型

  • 使用时间序列数据预测一个值

本章涵盖了循环神经网络(RNN)的各个方面,主要讲解、设计和构建不同的 RNN 模型。

介绍

如前一章所述,自然语言处理(NLP)是人工智能(AI)中的一个领域,涵盖了计算机如何理解和操作人类语言以执行有用的任务。如今,随着深度学习技术的增长,深度自然语言处理(深度 NLP)已成为一个新的研究领域。

那么,什么是深度自然语言处理?它是自然语言处理技术和深度学习的结合。结合这些技术的结果在以下领域取得了进展:

  • 语言学:语音转文本

  • 工具:词性标注、实体识别和句法分析

  • 应用:情感分析、问答、对话代理和机器翻译

深度 NLP 最重要的方法之一是对单词和句子的表示。单词可以表示为一个向量,位于充满其他单词的平面中。根据每个单词与另一个单词的相似度,其在平面中的距离会相应地设置为更大或更小。

图 4.1:多维度中的词语表示

图 4.1:多维度中的词语表示

上一张图展示了词嵌入的示例。词嵌入是一组技术和方法,将词语和句子从语料库映射到向量或实数。它生成了每个词语的表示,基于词语出现的上下文。然后,词嵌入可以找到词语之间的相似性。例如,离“dog”最近的词语如下:

  1. 老鼠

有多种方式可以生成词嵌入,例如 Word2Vec,它将在第七章中讲解,构建一个对话代理来管理机器人

这并不是深度学习对 NLP 在形态学层面带来的唯一巨大变化。通过深度学习,一个词可以表示为多个向量的组合。

每个语素都是一个向量,而一个词是通过组合多个语素向量得到的结果。

这种向量组合的技术也在语义层面上得到应用,但它用于单词和句子的创建。每个短语都是通过许多单词向量的组合形成的,因此一个句子可以表示为一个向量。

另一个改进是在句法分析方面。这项任务很困难,因为它具有歧义性。神经网络可以准确地确定句子的语法结构。

在应用的全面术语中,相关领域如下:

  • 情感分析:传统方法是将词汇包标记为正面或负面情感。然后,通过组合这些词汇来返回整个句子的情感。如今,利用深度学习和词表示模型,结果更为优秀。

  • 问题回答:为了找到问题的答案,向量表示可以将文档、段落或句子与输入问题匹配。

  • 对话代理:利用神经语言模型,模型可以理解查询并生成回应。

  • 机器翻译:机器翻译是 NLP 中最难的任务之一。已经尝试了许多方法和模型。传统模型非常庞大和复杂,但深度学习神经机器翻译解决了这个问题。句子通过向量进行编码,输出则进行解码。

词的向量表示是深度自然语言处理(NLP)的基础。创建一个平面,可以完成许多任务。在分析深度 NLP 技术之前,我们将回顾什么是循环神经网络(RNN),它在深度学习中的应用是什么,以及如何创建我们的第一个 RNN。

我们未来的对话代理将能够检测到对话的意图并作出预定义的回答。但是通过一个良好的对话数据集,我们可以创建一个循环神经网络(RNN)来训练一个能够根据对话主题生成回应的语言模型(LM)。这一任务也可以通过其他神经网络架构来实现,例如 seq2seq 模型。

循环神经网络

在本节中,我们将回顾循环神经网络RNNs)。本话题将首先介绍 RNN 的理论。它将回顾该模型中的多种架构,帮助你确定使用哪种模型来解决特定问题,还会探讨几种类型的 RNN 及其优缺点。此外,我们将了解如何创建一个简单的 RNN,训练它并进行预测。

循环神经网络(RNN)介绍

人类行为展示了各种有序的动作序列。人类能够基于一组先前的动作或序列来学习动态路径。这意味着人们并不是从零开始学习;我们有一些先前的知识,这有助于我们。例如,如果你不理解句子中的前一个词,你就无法理解下一个词!

传统上,神经网络无法解决这些类型的问题,因为它们不能学习先前的信息。那么,当问题无法仅通过当前信息解决时,应该怎么办呢?

1986 年,Michael I. Jordan 提出了一个处理时间组织经典问题的模型。该模型能够通过研究动态物体的先前运动来学习其轨迹。Jordan 创造了第一个 RNN。

图 4.2:非前置信息与时间序列的示例

图 4.2:非前置信息与时间序列的示例

在前一图中,左侧的图像告诉我们,如果没有任何信息,我们无法知道黑点的下一个动作会是什么。但如果我们假设它的前一个动作被记录为右侧图表中的红线,我们就能预测它的下一个动作。

递归神经网络内部

到目前为止,我们已经看到 RNN 与神经网络(NN)不同。RNN 的神经元就像普通神经元,但它们内部有循环,这使它们能够存储时间状态。通过存储某一时刻的状态,它们可以根据前一个时间状态进行预测。

图 4.3:传统神经元

图 4.3:传统神经元

前面的图显示了一个传统神经元,通常用于神经网络(NN)中。X**n是神经元的输入,在激活函数后,生成响应。RNN 神经元的架构则有所不同:

图 4.4:递归神经元

图 4.4:递归神经元

前图中的循环允许神经元存储时间状态。h**n是输入 X**n 和前一个状态的输出。神经元随着时间的推移而变化和演化。

如果神经元的输入是一个序列,那么展开的 RNN 将是这样的:

图 4.5:展开的递归神经元

图 4.5:展开的递归神经元

图 4.5 中的链状架构展示了 RNN 与序列和列表的紧密关系。因此,我们有与输入数量相同的神经元,每个神经元将其状态传递给下一个神经元。

RNN 架构

根据 RNN 中输入和输出的数量,有许多具有不同神经元数量的架构。每种架构都专门用于某个特定任务。到目前为止,已经有许多种网络类型:

图 4.6:RNN 的结构

图 4.6:RNN 的结构

前图展示了 RNN 的各种分类。书中早些时候,我们回顾了“一对一”架构。在本章中,我们将学习“多对一”架构。

  • 一对一:来自一个输入的分类或回归任务(图像分类)。

  • 一对多:图像描述任务。这些是深度学习中的难题。例如,传递图像作为输入的模型可以描述图像中的元素。

  • 多对一:时间序列,情感分析……每个任务只有一个输出,但基于一系列不同的输入。

  • 多对多:机器自动翻译系统。

  • 同步多对多:视频分类。

长期依赖问题

在某些任务中,预测模型的下一步只需要使用最新的信息。对于时间序列任务,需要检查更早的元素来学习或预测句子中的下一个元素或词语。例如,看看这句话:

  • The clouds are in the sky.

现在想象这个句子:

  • The clouds are in the [?]

你可能会假设需要的词是 sky,你知道这个是因为之前的信息:

  • 云层位于

但也有一些任务,模型需要使用先前的信息来获得更好的预测。例如,看看这句话:

  • 我出生在意大利,但 3 岁时搬到了法国……这就是我能说[?]的原因

为了预测词汇,模型需要从句子的开头获取信息,而这可能是一个问题。这是 RNN 的一个问题:当信息之间的距离较大时,学习变得更加困难。这个问题被称为 梯度消失

梯度消失问题

在 RNN 中,信息是随着时间流动的,因此前一步的信息会作为输入传递到下一步。每一步,模型都会计算代价函数,所以每次模型可能都会获得一个误差值。在通过网络传播误差,并在更新权重时尽量减小误差的过程中,操作结果会逐渐接近零(如果将两个小数相乘,结果会是一个更小的数)。这意味着模型的梯度在每次乘法操作后会变得越来越小。这里的问题是网络无法正确训练。解决 RNN 问题的方法之一是使用长短期记忆(LSTM)。

练习 14:使用 RNN 预测房价

我们将使用 Keras 创建我们的第一个 RNN。这个练习不是一个时间序列问题。我们将使用回归数据集来介绍 RNN。

我们可以使用 Keras 库中包含的多种方法来作为模型或层的类型:

  • Keras 模型:这些让我们可以使用 Keras 中的不同模型。我们将使用 Sequential 模型。

  • Keras 层:我们可以向神经网络中添加不同类型的层。在本次练习中,我们将使用 LSTM 和 Dense 层。Dense 层是神经网络中的常规神经元层,每个神经元都接收来自前一层所有神经元的输入,且这些神经元之间是密集连接的。

本次练习的主要目标是预测波士顿一所房子的价值,因此我们的数据集将包含每所房子的相关信息,比如房产的总面积或房间数量:

  1. sklearn导入波士顿房价数据集,并查看数据:

    from sklearn.datasets import load_boston
    boston = load_boston()
    boston.data
    

    图 4.7:波士顿房价数据

    图 4.7:波士顿房价数据
  2. 可以看到数据的数值很高,所以最好的做法是对数据进行归一化。使用 sklearnMinMaxScaler 函数,我们将把数据转换为 0 到 1 之间的数值:

    from sklearn.preprocessing import MinMaxScaler
    import numpy as np
    
    scaler = MinMaxScaler()
    x = scaler.fit_transform(boston.data)
    
    aux = boston.target.reshape(boston.target.shape[0], 1)
    y = scaler.fit_transform(aux)
    
  3. 将数据分为训练集和测试集。测试集的合理比例是数据的 20%:

    from sklearn.model_selection import train_test_split
    
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, shuffle=False)
    print('Shape of x_train {}'.format(x_train.shape))
    print('Shape of y_train {}'.format(y_train.shape))
    print('Shape of x_test {}'.format(x_test.shape))
    print('Shape of y_test {}'.format(y_test.shape))
    

    图 4.8:训练数据和测试数据的形状

    图 4.8:训练数据和测试数据的形状
  4. 导入 Keras 库并设置随机种子以初始化权重:

    import tensorflow as tf
    from keras.models import Sequential
    from keras.layers import Dense
    tf.set_random_seed(1)
    
  5. 创建一个简单的模型。密集层只是一个神经元集合。最后一层密集层只有一个神经元用于返回输出:

    model = Sequential()
    
    model.add(Dense(64, activation='relu'))
    model.add(Dense(32, activation='relu'))
    model.add(Dense(1))
    
    model.compile(loss='mean_squared_error', optimizer='adam')
    
  6. 训练网络:

    history = model.fit(x_train, y_train, batch_size=32, epochs=5, verbose=2)
    

    图 4.9:训练网络

    图 4.9:训练网络
  7. 计算模型的误差:

    error = model.evaluate(x_test, y_test)
    print('MSE: {:.5f}'.format(error))
    

    图 4.10:计算模型中的误差

    图 4.10:计算模型的误差
  8. 绘制预测结果:

    import matplotlib.pyplot as plt
    
    prediction = model.predict(x_test)
    print('Prediction shape: {}'.format(prediction.shape))
    
    plt.plot(range(len(x_test)), prediction.reshape(prediction.shape[0]), '--r')
    plt.plot(range(len(y_test)), y_test)
    plt.show()
    

图 4.11:我们模型的预测

图 4.11:我们模型的预测

现在你已经有了一个用于回归问题的 RNN!你可以尝试修改参数、添加更多层,或者改变神经元的数量来观察会发生什么。在下一个练习中,我们将使用 LSTM 层解决时间序列问题。

长短期记忆

LSTM 是一种 RNN,旨在解决长期依赖问题。它可以记住长时间或短时间的数值。它与传统的 RNN 的主要区别在于,它们包含一个单元或循环来内部存储记忆。

这种类型的神经网络是由 Hochreiter 和 Schmidhuber 于 1997 年创建的。这是一个 LSTM 神经元的基本结构:

图 4.12:LSTM 神经元结构

图 4.12:LSTM 神经元结构

正如你在前一张图中看到的,LSTM 神经元的结构是复杂的。它有三种类型的门控:

  • 输入门:允许我们控制输入值以更新记忆单元的状态。

  • 忘记门:允许我们擦除记忆单元中的内容。

  • 输出门:允许我们控制输入和记忆单元内容返回的值。

在 Keras 中,LSTM 模型具有三维输入:

  • 样本:是你拥有的数据量(序列的数量)。

  • 时间步:是你网络的记忆。换句话说,它存储之前的信息,以便做出更好的预测。

  • 特征:是每个时间步中的特征数量。例如,如果你处理的是图像,特征就是像素的数量。

    注:

    这种复杂的设计会导致另一种类型的网络的形成。这种新类型的神经网络是门控循环单元(GRU),它解决了消失梯度问题。

练习 15:预测数学函数的下一个解

在这个练习中,我们将构建一个 LSTM 来预测正弦函数的值。在这个练习中,你将学习如何使用 Keras 训练和预测一个 LSTM 模型。此外,这个练习还将介绍数据生成以及如何将数据划分为训练样本和测试样本:

  1. 使用 Keras,我们可以通过 Sequential 类创建一个 RNN,并且可以创建一个 LSTM 来添加新的循环神经元。导入 Keras 库来构建 LSTM 模型,导入 NumPy 来设置数据,导入 matplotlib 来绘制图表:

    import tensorflow as tf
    from keras.models import Sequential
    from keras.layers import LSTM, Dense
    import numpy as np
    import matplotlib.pyplot as plt
    
  2. 创建用于训练和评估模型的数据集。我们将生成一个包含 1000 个值的数组,作为正弦函数的结果:

    serie = 1000
    x_aux = [] #Natural numbers until serie
    x_aux = np.arange(serie)
    serie = (np.sin(2 * np.pi * 4 * x_aux / serie) + 1) / 2
    
  3. 为了查看数据是否合适,让我们绘制它:

    plt.plot(x_aux, serie)
    plt.show()
    

    图 4.13:带有绘制数据的输出

    图 4.13:带有绘制数据的输出
  4. 如本章所述,RNN 使用数据序列,因此我们需要将数据拆分为序列。在我们的例子中,序列的最大长度将为 5。这是必要的,因为 RNN 需要序列作为输入。

    该模型将是多对一的,因为输入是一个序列,而输出只是一个值。要理解为什么我们要使用多对一结构来创建 RNN,只需要了解输入和输出数据的维度:

    #Prepare input data
    maxlen = 5
    seq = []
    res = []
    for i in range(0, len(serie) - maxlen):
        seq.append(serie[i:maxlen+i])
        res.append(serie[maxlen+i])
    print(seq[:5])
    print(res[:5])
    
  5. 准备数据以将其输入到 LSTM 模型中。注意 xy 变量的形状。RNN 需要一个三维向量作为输入,一个二维向量作为输出。因此,我们将调整变量的形状:

    x = np.array(seq)
    y = np.array(res)
    x = x.reshape(x.shape[0], x.shape[1], 1)
    y = y.reshape(y.shape[0], 1)
    print('Shape of x {}'.format(x.shape))
    print('Shape of y {}'.format(y.shape))
    

    图 4.14:调整变量的形状

    图 4.14:调整变量的形状

    注意

    LSTM 的输入维度为 3。

  6. 将数据分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, shuffle=False)
    print('Shape of x_train {}'.format(x_train.shape))
    print('Shape of y_train {}'.format(y_train.shape))
    print('Shape of x_test {}'.format(x_test.shape))
    print('Shape of y_test {}'.format(y_test.shape))
    

    图 4.15:将数据拆分为训练集和测试集

    图 4.15:将数据拆分为训练集和测试集
  7. 构建一个简单的模型,其中包含一个 LSTM 单元和一个带有一个神经元的全连接层,使用线性激活函数。全连接层只是接收来自上一层输入并生成多个神经元输出的常规神经层。因此,我们的全连接层只有一个神经元,因为我们需要一个标量值作为输出:

    tf.set_random_seed(1)
    model = Sequential()
    model.add(LSTM(1, input_shape=(maxlen, 1)))   
    model.add(Dense(1, activation='linear'))      
    model.compile(loss='mse', optimizer='rmsprop')
    
  8. 训练模型 5 个周期(一个周期是神经网络处理整个数据集的过程),批次大小为 32,并进行评估:

    history = model.fit(x_train, y_train, batch_size=32, epochs=5, verbose=2)
    error = model.evaluate(x_test, y_test)
    print('MSE: {:.5f}'.format(error))
    

    图 4.16:使用 5 个周期和批次大小为 32 进行训练

    图 4.16:使用 5 个周期和批次大小为 32 进行训练
  9. 绘制测试预测结果,看看它是否表现良好:

    prediction = model.predict(x_test)
    print('Prediction shape: {}'.format(prediction.shape))
    plt.plot(range(len(x_test)), prediction.reshape(prediction.shape[0]), '--r')
    plt.plot(range(len(y_test)), y_test)
    plt.show()
    

    图 4.17:绘制预测结果的形状

    图 4.17:绘制预测结果的形状
  10. 让我们改进我们的模型。创建一个新的模型,LSTM 层中有四个单元,一个带有一个神经元的全连接层,并使用 sigmoid 激活函数:

    model2 = Sequential()
    model2.add(LSTM(4,input_shape=(maxlen,1)))
    model2.add(Dense(1, activation='sigmoid'))
    model2.compile(loss='mse', optimizer='rmsprop')
    
  11. 训练并评估模型 25 个周期,批次大小为 8:

    history = model2.fit(x_train, y_train,
                         batch_size=8,
                         epochs=25, 
                         verbose=1)
    error = model2.evaluate(x_test, y_test)
    print('MSE: {:.5f}'.format(error))
    

    图 4.18:使用 25 个周期和批次大小为 8 进行训练

    图 4.18:使用 25 个周期和批次大小为 8 进行训练
  12. 绘制模型的预测结果:

    predict_2 = model2.predict(x_test)
    predict_2 = predict_2.reshape(predict_2.shape[0]) 
    print(x_test.shape)
    plt.plot(range(len(x_test)),predict_2, '--r')
    plt.plot(range(len(y_test)), y_test)
    plt.show()
    

图 4.19:我们的神经网络的预测结果

图 4.19:我们的神经网络的预测结果

现在你可以比较每个模型的图表,我们可以看到第二个模型更好。通过这个练习,你已经掌握了 LSTM 的基础知识,学会了如何训练和评估你创建的模型,以及如何判断它是否好。

神经语言模型

第三章自然语言处理基础 向我们介绍了统计语言模型(LMs),即一个单词序列的概率分布。我们知道语言模型可以用来预测句子中的下一个单词,或者计算下一个单词的概率分布。

图 4.20:用于计算下一个单词概率分布的语言模型公式

图 4.20:用于计算下一个单词概率分布的语言模型公式

单词序列是x1x2 … 下一个单词是x**t+1w**j是词汇表中的一个词。V是词汇表,j是词汇表中词的位置信息。w**j是位于位置j的词。

你每天都在使用语言模型(LM)。手机上的键盘使用此技术来预测句子的下一个单词,像谷歌这样的搜索引擎也使用它来预测你想要搜索的内容。

我们讨论了 n-gram 模型和通过计数语料库中的词语来计算 bigram,但该解决方案有一些局限性,如长依赖性。深度 NLP 和神经语言模型将有助于绕过这些局限性。

神经语言模型简介

神经语言模型遵循与统计语言模型相同的结构。它们的目标是预测句子中的下一个单词,但方式不同。神经语言模型的灵感来源于 RNN,因为使用了序列作为输入。

练习 15预测数学函数的下一个解,通过前五个步骤的序列来预测正弦函数的下一个结果。在这种情况下,数据不是正弦函数结果的序列,而是单词,模型将预测下一个单词。

这些神经语言模型(LMs)源于改善统计方法的需求。新的模型可以绕过一些传统语言模型的局限性和问题。

统计语言模型的问题

在前一章中,我们回顾了语言模型以及 n-gram、bigram 和马尔可夫模型的概念。这些方法通过计数文本中的词频来执行。这就是为什么这些方法被称为统计语言模型的原因。

语言模型的主要问题是数据限制。如果我们要计算的句子的概率分布在数据中不存在,该怎么办?一个部分解决方案是平滑方法,但它不足够。

另一种解决方案是使用马尔可夫假设(每个概率仅依赖于前一个步骤,从而简化链式法则)来简化句子,但这不会给出好的预测。这意味着我们可以使用 3-grams 来简化我们的模型。

解决这个问题的一种方法是增加语料库的大小,但语料库最终会变得过大。这种 n-gram 模型的局限性被称为稀疏性问题

基于窗口的神经模型

这个新模型的第一次近似是使用滑动窗口来计算下一个单词的概率。这个解决方案的概念来自于窗口分类。

就单词而言,没有上下文,很难理解一个单词的含义。如果这个单词没有出现在句子或段落中,会出现很多问题,例如两个相似单词之间的歧义或自反义词问题。自反义词是具有多重含义的词。比如单词 "handicap" ,根据上下文,它可以表示优势(例如,在运动中)或劣势(有时具有冒犯意味,是一种身体问题)。

窗口分类法通过其邻近单词的上下文(由窗口创建)对单词进行分类。可以使用滑动窗口的方法生成语言模型。以下是一个图形示例:

图 4.21:基于窗口的神经语言模型

图 4.21:基于窗口的神经语言模型

在前面的图中,展示了基于窗口的神经模型是如何工作的。窗口大小为 5(word1 到 word5)。它创建一个向量,将每个单词的嵌入向量连接起来,并在隐藏层中进行计算:

图 4.22:隐藏层公式

图 4.22:隐藏层公式

最后,为了预测一个单词,模型返回一个可以用来分类该单词概率的值:

图 4.23:Softmax 函数

图 4.23:Softmax 函数

然后,具有最高值的单词将是预测的单词。

我们不打算深入探讨这些术语,因为我们将使用 LSTM 来创建语言模型。

相比传统方法,这种方法的优势如下:

  • 更少的计算工作。基于窗口的神经模型需要更少的计算资源,因为它们不需要遍历语料库计算概率。

  • 它避免了通过改变 N-gram 的维度来寻找好的概率分布的问题。

  • 生成的文本在意义上会更有逻辑,因为这种方法解决了稀疏性问题。

但是存在一些问题:

  • 窗口的限制:窗口的大小不能太大,否则一些单词的含义可能会出错。

  • 每个窗口都有自己的权重值,因此可能会导致歧义。

  • 如果窗口的大小增加,模型也会随之增长。

分析窗口模型的问题时,RNN 可以提高性能。

RNN 语言模型

RNN 能够计算序列中下一个单词的概率,基于之前步骤的信息。该方法的核心思想是,在整个训练过程中反复应用相同的权重。

使用 RNN 语言模型相对于基于窗口的模型有一些优势:

  • 这种架构可以处理任意长度的句子;它没有固定的大小,不像基于窗口的方法。

  • 对于每个输入大小,模型是相同的。如果输入更大,它不会增长。

  • 根据神经网络架构,它可以使用来自前一步和后一步的信息。

  • 权重在各个时间步之间是共享的。

到目前为止,我们已经讨论了改进统计语言模型的不同方法以及每种方法的优缺点。在开发 RNN 语言模型之前,我们需要了解如何将句子作为输入引入神经网络。

独热编码

神经网络和机器学习都是关于数字的。正如我们在本书中所看到的,输入元素是数字,输出是编码标签。但如果神经网络的输入是一个句子或一组字符,如何将其转化为数值呢?

独热编码是离散变量的数值表示。它假设对于离散变量集中的不同值,特征向量的大小相同。这意味着如果语料库的大小为 10,每个单词将被编码为一个长度为 10 的向量。因此,每个维度对应集合中的一个唯一元素。

图 4.24:RNN 数据预处理流

图 4.24:RNN 数据预处理流

前一张图展示了独热编码的工作原理。理解每个向量的形状非常重要,因为神经网络需要了解我们拥有的输入数据以及我们希望获得的输出。接下来,练习 16,编码一个小语料库 将帮助你更详细地学习独热编码的基础。

练习 16:编码一个小语料库

在本练习中,我们将学习如何使用独热编码对一组单词进行编码。这是最基本的编码方法,它为我们提供了离散变量的表示。

本练习将涵盖执行此任务的不同方法。一种方法是手动执行编码,另一种方法是使用库。在完成练习后,我们将获得每个单词的向量表示,准备作为神经网络的输入:

  1. 定义一个语料库。这个语料库与我们在第三章自然语言处理基础中使用的语料库相同:

    corpus = [
         'My cat is white',
         'I am the major of this city',
         'I love eating toasted cheese',
         'The lazy cat is sleeping',
    ]
    
  2. 使用 spaCy 对其进行分词。我们不会使用停用词(去除无用的词,如冠词)方法,因为我们有一个小语料库。我们希望保留所有的标记:

    import spacy
    import en_core_web_sm
    nlp = en_core_web_sm.load()
    
    corpus_tokens = []
    for c in corpus:
        doc = nlp(c)
        tokens = []
        for t in doc:
            tokens.append(t.text)
        corpus_tokens.append(tokens)
    corpus_tokens
    
  3. 创建一个包含语料库中每个唯一标记的列表:

    processed_corpus = [t for sentence in corpus_tokens for t in sentence]
    processed_corpus = set(processed_corpus)
    processed_corpus
    

    图 4.25:语料库中每个唯一标记的列表

    图 4.25:语料库中每个唯一标记的列表
  4. 创建一个字典,其中每个单词在语料库中作为键,唯一的数字作为值。这个字典将类似于 {word:value},并且该值将在独热编码向量中具有索引 1:

    word2int = dict([(tok, pos) for pos, tok in enumerate(processed_corpus)])
    word2int
    

    图 4.26:每个单词作为键,唯一的数字作为值

    图 4.26:每个单词作为键,唯一的数字作为值
  5. 对句子进行编码。这种编码方式是手动的。有一些库,如 sklearn,提供自动编码方法:

    Import numpy as np
    sentence = 'My cat is lazy'
    tokenized_sentence = sentence.split()
    encoded_sentence = np.zeros([len(tokenized_sentence),len(processed_corpus)])
    encoded_sentence
    for i,c in enumerate(sentence.split()):
        encoded_sentence[i][ word2int[c] ] = 1
    encoded_sentence
    

    图 4.27:手动独热编码向量
    print("Shape of the encoded sentence:", encoded_sentence.shape)
    
  6. 导入 sklearn 方法。sklearn 首先使用 LabelEncoder 对语料库中的每个唯一标记进行编码,然后使用 OneHotEncoder 创建向量:

    from sklearn.preprocessing import LabelEncoder
    from sklearn.preprocessing import OneHotEncoder
    Declare the LabelEncoder() class.
    le = LabelEncoder()
    Encode the corpus with this class.
    labeled_corpus = le.fit_transform(list(processed_corpus))
    labeled_corpus
    

    图 4.28:使用 OneHotEncoder 创建的向量
  7. 现在,使用之前编码的相同句子,并应用我们创建的 LabelEncoder 转换方法:

    sentence = 'My cat is lazy'
    tokenized_sentence = sentence.split()
    integer_encoded = le.transform(tokenized_sentence)
    integer_encoded
    

    图 4.29:应用 LabelEncoder 转换

    图 4.29:应用 LabelEncoder 转换
  8. 我们可以解码 LabelEncoder 中的初始句子:

    le.inverse_transform(integer_encoded)
    

    图 4.30:解码后的 LabelEncoder

    图 4.30:解码后的 LabelEncoder
  9. 使用 sparse=False 声明 OneHotEncoder(如果不指定此项,它将返回一个稀疏矩阵):

    onehot_encoder = OneHotEncoder(sparse=False)
    
  10. 为了使用我们创建的标签编码器编码句子,我们需要将带标签的语料库重塑以适应 onehot_encoder 方法:

    labeled_corpus = labeled_corpus.reshape(len(labeled_corpus), 1)
    onehot_encoded = onehot_encoder.fit(labeled_corpus)
    
  11. 最后,我们可以将我们的句子(使用 LabelEncoder 编码)转换成一个 one-hot 向量。这种编码方式与手动编码的结果不会完全相同,但它们会具有相同的形状:

    sentence_encoded = onehot_encoded.transform(integer_encoded.reshape(len(integer_encoded), 1))
    print(sentence_encoded)
    

图 4.31:使用 Sklearn 方法进行的 One-hot 编码向量

图 4.31:使用 Sklearn 方法进行的 One-hot 编码向量

注意

这个练习非常重要。如果你不理解矩阵的形状,理解 RNN 的输入将会非常困难。

干得好!你完成了练习 16。现在你可以将离散变量编码成向量了。这是训练和评估神经网络的预处理数据的一部分。接下来,我们将进行本章的活动,目标是使用 RNN 和 one-hot 编码创建一个语言模型(LM)。

注意

对于较大的语料库,one-hot 编码不是很有用,因为它会为每个词创建巨大的向量。因此,通常使用嵌入向量。这个概念将在本章稍后介绍。

RNN 的输入维度

在开始 RNN 活动之前,你可能不了解输入维度。在本节中,我们将重点理解 n 维数组的形状,以及如何添加或删除一个维度。

序列数据格式

我们之前提到了多对一架构,其中每个样本由一个固定的序列和一个标签组成。这个标签对应序列中的下一个值。就像这样:

图 4.32:序列数据的格式

图 4.32:序列数据的格式

在这个例子中,我们在矩阵 X 中有两个序列,Y 中有两个输出标签。因此,形状如下:

X = (2, 4)

Y = (2)

但如果你尝试将这些数据输入到 RNN 中,它将无法正常工作,因为它没有正确的维度。

RNN 数据格式

为了在 Keras 中实现带有时间序列的 RNN,模型将需要一个具有三维的输入向量,并且输出一个二维的向量。

所以,对于 X 矩阵,我们将得到以下形状:

  • 样本数量

  • 序列长度

  • 值的长度

图 4.33:RNN 数据格式

图 4.33:RNN 数据格式

这里的形状如下:

X = (2, 4, 1)

Y = (2, 1)

One-hot 格式

使用 one-hot 编码后,我们的输入维度相同,但值的长度发生了变化。在前面的图中,我们可以看到值([1],[2],…)是单维的。但使用 one-hot 编码后,这些值将变成向量,因此形状将如下所示:

图 4.34:One-hot 格式

图 4.34:One-hot 格式

X = (2, 4, 3)

Y = (2, 3)

为了执行所有这些维度变化,将使用 NumPy 库中的reshape方法。

注意

通过了解维度的知识,你可以开始进行活动,记住,LSTM 的输入维度是三,而输出维度是二。那么,如果你连续创建两层 LSTM,如何将第三个维度添加到第一层的输出中呢?将返回状态设置为 True。

活动 4:预测序列中的下一个字符

在这个活动中,我们将预测长序列中的下一个字符。这个活动必须使用 one-hot 编码来创建输入和输出向量。模型的架构将是 LSTM,就像我们在练习 14中看到的那样,使用 RNN 预测房价

场景:你在一家全球公司担任安全经理。某天早晨,你发现黑客已经发现并更改了公司数据库的所有密码。你和你的工程师团队开始尝试解码黑客的密码,以进入系统并修复所有问题。分析所有新密码后,你发现了一个共同的结构。

你只需要解码密码中的一个字符,但你不知道那个字符是什么,而且你只有一次机会输入正确的密码。

然后,你决定创建一个程序,分析长序列的数据以及你已经知道的五个密码字符。通过这些信息,它可以预测密码的最后一个字符。

密码的前五个字符是:tyuio。最后一个字符会是什么?

注意

你必须使用 one-hot 编码和 LSTM。你将使用 one-hot 编码向量训练模型。

  1. 这是数据序列:qwertyuiopasdfghjklñzxcvbnm

    注意

    这个序列重复了 100 次,所以这样做:sequence = 'qwertyuiopasdfghjklñzxcvbnm' * 100。

  2. 将数据划分为五个字符一组,并准备输出数据。

  3. 将输入和输出序列编码为 one-hot 编码向量。

  4. 设置训练数据和测试数据。

  5. 设计模型。

    注意

    输出包含许多零,因此很难达到精确的结果。使用 LeakyRelu 激活函数,alpha 值为 0.01,当进行预测时,将该向量的值四舍五入。

  6. 训练并评估它。

  7. 创建一个函数,当给定五个字符时,预测下一个字符,以便找出密码的最后一个字符。

    注意

    该活动的解决方案可以在第 308 页找到。

总结

AI 和深度学习在图像和人工视觉方面正在取得巨大进展,这要归功于卷积网络。但 RNN 也拥有强大的能力。

在这一章中,我们回顾了神经网络如何使用时间序列来预测正弦函数的值。如果你改变训练数据,这种架构可以学习每个分布的股票走势。此外,RNN 有许多不同的架构,每种架构都针对特定任务进行了优化。但是,RNN 存在梯度消失的问题。解决这个问题的一种方法是新模型——长短期记忆网络(LSTM),它通过改变神经元的结构来记住时间步长。

聚焦于语言学,统计语言模型(LM)存在许多与计算负载和分布概率相关的问题。为了解决稀疏性问题,n-gram 模型的大小被降低到 4 或 3 个 gram,但这个步长不足以预测下一个词。如果我们使用这种方法,稀疏性问题仍然会出现。一个具有固定窗口大小的神经语言模型(LM)可以避免稀疏性问题,但仍然存在窗口大小有限和权重的限制问题。使用 RNN 时,这些问题不会出现,并且根据架构的不同,它可以获得更好的结果,能够向前和向后看很多步。但是深度学习是关于向量和数字的。当你想要预测单词时,你需要对数据进行编码以训练模型。有多种不同的方法,例如独热编码(one-hot encoder)或标签编码(label encoder)。现在,你可以从已训练的语料库和 RNN 中生成文本。

在下一章中,我们将讨论卷积神经网络(CNN)。我们将回顾 CNN 的基本技术和架构,并进一步探讨更复杂的实现方法,例如迁移学习。

第六章:第五章

计算机视觉中的卷积神经网络

学习目标

在本章结束时,你将能够:

  • 解释卷积神经网络的工作原理

  • 构建卷积神经网络

  • 通过使用数据增强来改进已构建的模型

  • 通过实现迁移学习使用最先进的模型

在本章中,我们将学习如何使用概率分布作为一种无监督学习的形式。

引言

在上一章中,我们学习了神经网络如何通过训练预测值,并了解了基于其架构的 递归神经网络(RNN) 在许多场景中的应用价值。本章中,我们将讨论并观察 卷积神经网络(CNNs) 如何与密集神经网络(也称为全连接神经网络,如第二章《计算机视觉导论》中所提到的)以类似的方式工作。

CNNs 拥有具有权重和偏置的神经元,这些权重和偏置会在训练过程中进行更新。CNNs 主要用于图像处理。图像被解释为像素,网络输出它认为图像所属的类别,以及损失函数,后者描述每次分类和每次输出的错误。

这类网络假设输入是图像或类似图像的形式,这使得它们可以更高效地工作(CNNs 比深度神经网络更快、更好)。在接下来的章节中,你将了解更多关于 CNNs 的内容。

CNNs 基础

在本主题中,我们将看到 CNNs 如何工作,并解释卷积图像的过程。

我们知道图像是由像素组成的,如果图像是 RGB 格式,例如,它将有三个通道,每个字母/颜色(红-绿-蓝)都有自己的一组像素,每个通道的像素大小相同。全连接神经网络并不会在每一层表示图像的这种深度,而是通过一个单一的维度来表示这种深度,这显然是不够的。此外,它们将每一层的每个神经元与下一层的每个神经元连接,依此类推。这反过来导致性能较低,意味着你需要训练网络更长的时间,但仍然无法获得良好的结果。

CNNs 是一类神经网络,在分类和图像识别等任务中非常有效。虽然它们也能很好地处理声音和文本数据。CNNs 由输入层、隐藏层和输出层组成,就像普通的神经网络一样。输入层和隐藏层通常由 卷积层池化层(用于减少输入的空间尺寸)和 全连接层(全连接层将在第二章《计算机视觉导论》中解释)构成。卷积层和池化层将在本章稍后进行详细讲解。

CNN(卷积神经网络)为每一层赋予了深度,从图像的原始深度到更深的隐藏层。下图展示了 CNN 的工作原理以及其结构:

图 5.1:CNN 的表示

图 5.1:CNN 的表示

在前面的图中,CNN 接收一个 224 x 224 x 3 的输入图像,通过卷积处理后转化为下一个层级,这一过程压缩了尺寸,但深度增加(我们稍后会解释这些过程是如何工作的)。这些操作会不断进行,直到图形表示被拉平,并通过这些密集层来输出数据集对应的类别。

卷积层:卷积层由一组固定大小(通常较小)的滤波器组成,这些滤波器是具有特定值/权重的矩阵,它们会遍历输入(例如一张图像),通过计算滤波器与输入之间的标量积,这个过程称为卷积。每个滤波器会生成一个二维激活图,这些激活图会沿着输入的深度堆叠。激活图的作用是寻找输入中的特征,并决定网络学习的效果。滤波器的数量越多,层的深度越大,因此网络学习得越多,但训练时的速度会变慢。例如,在某个图像中,你可能希望在第一层使用 3 个滤波器,在下一层使用 96 个滤波器,在再下一层使用 256 个滤波器,依此类推。请注意,在网络的开始部分,滤波器通常比在中间或末端部分少。这是因为网络的中间和末端具有更多潜在的特征可以提取,因此我们需要更多、更小的滤波器来处理网络的后段。这是因为随着我们深入网络,我们更多地关注图像中的小细节,因此希望从这些细节中提取更多特征,以便更好地理解图像。

卷积层滤波器的尺寸通常从 2x2 到 7x7 不等,具体取决于你处于网络的哪个阶段(开始部分使用较大尺寸,末端使用较小尺寸)。

在图 5.1 中,我们可以看到使用滤波器(浅蓝色)进行卷积操作,输出将是一个单一值,并传递到下一步/层。

在进行卷积操作后,在应用另一次卷积之前,通常会使用最大池化(池化层)来减少输入的大小,以便网络能够对图像进行更深层次的理解。然而,近年来,逐渐避免使用最大池化,转而鼓励使用步幅,这是在进行卷积时自然应用的,因此我们将通过自然地应用卷积来解释图像尺寸的缩小。

步长(Strides):这是定义为像素的长度,用于描述滤波器在整个图像上应用的步伐。如果选择步长为 1,滤波器将每次应用一个像素。同样,如果选择步长为 2,则滤波器将每次应用两个像素,输出大小会比输入小,依此类推。

让我们来看一个例子。首先,将使用图 5.2 作为滤波器对图像进行卷积,它是一个 2x2 的矩阵:

图 5.2: 卷积滤波器

图 5.2: 卷积滤波器

以下可能是我们正在卷积的图像(矩阵):

图 5.3: 要卷积的图像

图 5.3: 要卷积的图像

当然,这不是一个真实的图像,但为了简化,我们使用一个 4x4 的随机值矩阵来演示卷积是如何工作的。

现在,如果我们要应用步长为 1 的卷积,图形化的过程如下:

图 5.4: 卷积过程 Stride=1

图 5.4: 卷积过程 Stride=1

上面的图显示了一个 2x2 的滤波器被逐像素地应用到输入图像上。过程是从左到右、从上到下进行的。

滤波器将矩阵中每个位置的每个值与它所应用区域(矩阵)中每个位置的每个值相乘。例如,在过程的第一部分,滤波器应用于图像的前 2x2 部分 [1 2; 5 6],而我们使用的滤波器是 [2 1; -1 2],那么计算方式为 12 + 21 + 5(-1) + 62 = 11。

应用滤波器矩阵后,得到的图像如下所示:

图 5.5: 卷积结果 Stride=1

图 5.5: 卷积结果 Stride=1

如你所见,处理后的图像现在变得比原来少了一维。这是因为存在一个名为padding的参数,默认设置为“valid”,这意味着卷积会按常规应用;也就是说,应用卷积后,图像自然会变得薄一像素。如果设置为“same”,图像周围将被一行像素包围,像素值为零,因此输出矩阵的大小将与输入矩阵相同。

现在,我们将应用步长为 2 的卷积,以将图像大小减少 2(就像一个 2x2 的最大池化层所做的那样)。请记住,我们使用的是“valid”类型的 padding。

该过程将会减少步骤,就像以下图所示:

图 5.6: 卷积过程 Stride=2

图 5.6: 卷积过程 Stride=2

而输出的图像/矩阵看起来会是这样的:

图 5.7: 卷积结果 Stride=2

图 5.7: 卷积结果 Stride=2

结果图像将是一个 2x2 像素的图像。这是由于卷积过程的自然结果,步长(stride)为 2。

这些在每个卷积层上应用的过滤器,具有权重,神经网络会调整这些权重,使得这些过滤器的输出有助于神经网络学习有价值的特征。如前所述,这些权重通过反向传播过程更新。提醒一下,反向传播是网络计算训练步骤中预测结果与期望结果之间的误差(或损失),然后更新所有贡献于该误差的神经元权重,以避免再次犯同样的错误。

构建你的第一个 CNN

注意

本章我们仍然将在 TensorFlow 上使用 Keras 作为后端,正如在本书的第二章《计算机视觉导论》中所提到的。此外,我们仍然会使用 Google Colab 来训练我们的网络。

Keras 是一个非常好的实现卷积层的库,因为它对用户进行了抽象,用户不需要手动实现各层。

第二章《计算机视觉导论》中,我们通过使用keras.layers包导入了 Dense、Dropout 和 BatchNormalization 层,而为了声明二维卷积层,我们将使用相同的包:

from keras.layers import Conv2D

Conv2D模块与其他模块类似:你必须声明一个顺序模型,这在本书的第二章《计算机视觉导论》中已做解释,我们还需要添加Conv2D

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), padding='same', strides=(2,2), input_shape=input_shape))

对于第一层,必须指定输入形状,但之后不再需要。

必须指定的第一个参数是过滤器的数量,即网络在该层要学习的过滤器数量。如前所述,在较早的层中,我们会学习少量的过滤器,而不是网络中的深层过滤器。

必须指定的第二个参数是卷积核大小(kernel size),即应用于输入数据的滤波器大小。通常设置为 3x3 的卷积核,或者甚至是 2x2 的卷积核,但有时当图像较大时,会使用更大的卷积核。

第三个参数是padding,默认设置为“valid”,但我们需要将其设置为“same”,因为我们希望保持输入的尺寸,以便理解输入的下采样行为。

第四个参数是strides,默认设置为(1, 1)。我们将其设置为(2, 2),因为这里有两个数字,并且需要为 x 轴和 y 轴都设置该参数。

在第一层之后,我们将采用与第二章《计算机视觉导论》中提到的相同方法:

model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.2))

提醒一下,批量归一化(BatchNormalization)层用于规范化每层的输入,帮助网络更快收敛,通常也能提高整体效果。

激活函数是一个接受输入并计算其加权和的函数,添加偏置后决定是否激活(分别输出 1 和 0)。

Dropout 层通过关闭一定比例的神经元,帮助网络避免过拟合,过拟合是指训练集的准确率远高于验证集的准确率。

我们可以像这样应用更多的层集,根据问题的大小调整参数。

最后一层和密集神经网络的层保持一致,具体取决于问题。

练习 17:构建一个 CNN

注意

这个练习使用了与第二章,计算机视觉简介相同的包和库。这些库包括 Keras、Numpy、OpenCV 和 Matplotlib。

在这个练习中,我们将使用与第二章活动 2分类 Fashion-MNIST 数据库中的 10 种衣物类型相同的问题。

记住,在那个活动中,构建的神经网络并不能很好地泛化,以至于无法对我们传递给它的未见数据进行分类。

提醒一下,这个问题是一个分类问题,模型需要正确地分类 10 种类型的衣物:

  1. 打开你的 Google Colab 界面。

  2. 为书籍创建一个文件夹,并从 GitHub 下载Datasets文件夹并上传到你驱动器中的文件夹里。

  3. 挂载驱动器并按照以下方式进行:

    from google.colab import drive
    drive.mount('/content/drive')
    

    注意

    每次使用新合作者时,将驱动器挂载到目标文件夹。

  4. 一旦你首次挂载了你的驱动器,你将需要输入 Google 给出的授权代码,点击给出的 URL 并按下键盘上的Enter键:图 5.8:在 Google Collab 上挂载

    图 5.8:在 Google Collab 上挂载
  5. 现在你已经挂载了驱动器,需要设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson05/
    

    注意

    第 5 步中提到的路径可能会根据你在 Google Drive 上的文件夹设置而有所变化。路径将始终以cd /content/drive/My Drive/开头。

  6. 首先,让我们从 Keras 导入数据并将随机种子初始化为 42,以确保结果可复现:

    from keras.datasets import fashion_mnist 
    (x_train, y_train), (x_test, y_test) =fashion_mnist.load_data()
    import random
    random.seed(42) 
    
  7. 我们导入了 NumPy 以便对数据进行预处理,并导入 Keras 工具来进行标签的独热编码:

    import numpy as np
    from keras import utils as np_utils
    x_train = (x_train.astype(np.float32))/255.0 
    x_test = (x_test.astype(np.float32))/255.0 
    x_train = x_train.reshape(x_train.shape[0], 28, 28, 1) 
    x_test = x_test.reshape(x_test.shape[0], 28, 28, 1) 
    y_train = np_utils.to_categorical(y_train, 10) 
    y_test = np_utils.to_categorical(y_test, 10) 
    input_shape = x_train.shape[1:]
    
  8. 我们声明Sequential函数来创建 Keras 的顺序模型、回调函数,当然还有层:

    from keras.models import Sequential
    from keras.callbacks import EarlyStopping, ModelCheckpoint
    from keras.layers import Input, Dense, Dropout, Flatten
    from keras.layers import Conv2D, Activation, BatchNormalization
    

    注意

    我们已经导入了一个回调函数叫做EarlyStopping。这个回调的作用是,当你选择的度量(例如,验证准确率)下降时,在指定的轮次后停止训练。你可以通过设置想要的轮次数来确定这个数字。

  9. 现在,我们将构建我们的第一个 CNN。首先,让我们将模型声明为Sequential并添加第一个Conv2D

    def CNN(input_shape):
        model = Sequential()
        model.add(Conv2D(32, kernel_size=(3, 3), padding='same', strides=(2,2), input_shape=input_shape))
    

    我们添加了 32 个滤波器作为第一层,滤波器的大小为 3x3。填充设置为“same”,步长设置为 2,以自然地减少Conv2D模块的维度。

  10. 我们通过添加ActivationBatchNormalization层来继续这个层:

        model.add(Activation('relu'))
        model.add(BatchNormalization())
    
  11. 我们将添加另外三层,保持之前相同的特性,应用 dropout,并跳到另一个模块:

        model.add(Conv2D(32, kernel_size=(3, 3), padding='same', strides=(2,2)))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
    
  12. 现在,我们应用 20%的 dropout,这会关闭网络中 20%的神经元:

        model.add(Dropout(0.2))
    
  13. 我们将再次进行相同的操作,但这次使用 64 个过滤器:

        model.add(Conv2D(64, kernel_size=(3, 3), padding='same', strides=(2,2)))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Conv2D(64, kernel_size=(3, 3), padding='same', strides=(2,2)))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
    
  14. 对于网络的最后部分,我们应用Flatten层将最后一层的输出转换为一维。我们应用一个包含 512 个神经元的Dense层。在网络的物流部分,我们应用Activation层和BatchNormalization层,然后应用 50%的Dropout

        model.add(Flatten())
        model.add(Dense(512))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.5))
    
  15. 最后,我们声明最后一层为一个包含 10 个神经元的dense层,这是数据集的类别数,并应用Softmax激活函数,确定图像最可能属于哪个类别,最后返回模型:

        model.add(Dense(10, activation="softmax"))
        return model
    
  16. 让我们声明模型并加上回调函数,然后进行编译:

    model = CNN(input_shape)
    
    model.compile(loss='categorical_crossentropy', optimizer='Adadelta', metrics=['accuracy'])
    
    ckpt = ModelCheckpoint('Models/model.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False) 
    earlyStopping = EarlyStopping(monitor='val_loss', patience=5, verbose=0,mode='min')
    

    对于编译,我们使用相同的优化器。对于声明检查点,我们使用相同的参数。对于声明EarlyStopping,我们将验证损失作为主要度量,并设置耐心值为五个 epoch。

  17. 让训练开始吧!

    model.fit(x_train, y_train, batch_size=128, epochs=100, verbose=1, validation_data=(x_test, y_test), callbacks=[ckpt,earlyStopping]) 
    

    我们将批量大小设置为 128,因为图像数量足够,而且这样做会减少训练时间。epoch 的数量设置为 100,因为EarlyStopping会负责停止训练。

    所得到的准确率比第二章中的练习计算机视觉简介中的结果更好——我们获得了92.72%的准确率。

    请查看以下输出:

    图 5.9:显示的 val_acc 为 0.9240,即 92.72%

    注意

    本次练习的完整代码可以在 GitHub 上找到:github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson05/Exercise17/Exercise17.ipynb

  18. 让我们尝试使用我们在第二章活动 2,即Fashion-MNIST 数据库的 10 种服装分类中尝试过的相同例子,数据位于Dataset/testing/

    import cv2 
    
    images = ['ankle-boot.jpg', 'bag.jpg', 'trousers.jpg', 't-shirt.jpg'] 
    
    for number in range(len(images)):
        imgLoaded = cv2.imread('Dataset/testing/%s'%(images[number]),0) 
        img = cv2.resize(imgLoaded, (28, 28)) 
        img = np.invert(img) 
        img = (img.astype(np.float32))/255.0 
        img = img.reshape(1, 28, 28, 1) 
    
        plt.subplot(1,5,number+1),plt.imshow(imgLoaded,'gray') 
        plt.title(np.argmax(model.predict(img)[0])) 
        plt.xticks([]),plt.yticks([]) 
    plt.show()
    

这是输出:

图 5.10:使用 CNN 进行服装预测

图 5.10:使用卷积神经网络(CNN)进行服装预测

提醒一下,这是对应服装数量的表格:

图 5.11:对应服装数量的表格

图 5.11:对应服装数量的表格

我们可以看到模型已经很好地预测了所有图片,因此我们可以断定这个模型远比只有密集层的模型要好。

改进您的模型 - 数据增强

有时候,你可能无法通过构建更好的模型来提高模型的准确性。有时,问题不在于模型,而在于数据。工作时最重要的事情之一是,使用的数据必须足够好,以便潜在的模型能够对这些数据进行泛化。

数据可以表示现实中的事物,但也可能包含表现不佳的错误数据。当数据不完整或数据无法很好地代表各类时,就可能会发生这种情况。对于这些情况,数据增强已成为最流行的方法之一。

数据增强实际上是增加原始数据集的样本数量。对于计算机视觉,这意味着增加数据集中的图像数量。数据增强技术有很多种,你可能会根据数据集的不同选择特定的技术。这里提到了一些技术:

  • 旋转:用户为数据集中的图像设置旋转角度。

  • 翻转:水平或垂直翻转图像。

  • 裁剪:从图像中随机裁剪一部分。

  • 改变颜色:更改或变化图像的颜色。

  • 添加噪声:向图像中添加噪声。

应用这些或其他技术,你将生成与原始图像不同的新图像。

为了在代码中实现这一点,Keras 有一个叫做ImageDataGenerator的模块,在其中声明你希望应用到数据集的变换。你可以通过以下代码行导入该模块:

from keras.preprocessing.image import ImageDataGenerator

为了声明将应用所有这些更改的数据集变量,你需要像下面的代码片段一样声明它:

datagen = ImageDataGenerator(
        rotation_range=20,
        zoom_range = 0.2,
        width_shift_range=0.1,
        height_shift_range=0.1,
        horizontal_flip=True
        )

注意

你可以通过查看 Keras 的这篇文档,了解可以传递给ImageDataGenerator的属性:keras.io/preprocessing/image/

在声明datagen之后,你需要使用以下方法进行特征-wise 规范化计算:

datagen.fit(x_train)

这里,x_train是你的训练集。

为了使用数据增强训练模型,应使用以下代码:

model.fit_generator(datagen.flow(x_train, y_train,
                                 batch_size=batch_size),
                    epochs=epochs,
                    validation_data=(x_test, y_test),
                    callbacks=callbacks,
                    steps_per_epoch=len(x_train) // batch_size)

Datagen.flow()用于应用数据增强。由于 Keras 不知道何时停止对给定数据应用数据增强,Steps_per_epoch是设置该限制的参数,应该是训练集长度除以批量大小。

现在我们将直接进入本章的第二个练习,观察输出。数据增强承诺更好的结果和更高的准确度。让我们来看看这是否成立。

练习 18:使用数据增强改进模型

在本练习中,我们将使用牛津-III 宠物数据集,该数据集包含 RGB 图像,大小不一,有多个类,包括不同品种的猫和狗。在这个例子中,我们将数据集分为两个类:猫和狗,为了简便。每个类有 1,000 张图片,虽然数量不多,但这将增强数据增强的效果。该数据集存储在 GitHub 上Dataset/dogs-cats/文件夹中。

我们将构建一个 CNN,并分别使用和不使用数据增强进行训练,然后比较结果:

注意

对于这个练习,我们将打开另一个 Google Colab 笔记本。

此练习的完整代码可以在 GitHub 上找到:github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson05/Exercise18/Exercise18.ipynb

  1. 打开您的 Google Colab 界面。

  2. 创建一个文件夹用于存放书籍,并从 GitHub 下载Datasets文件夹并上传到您驱动器中的该文件夹。

  3. 导入驱动器并按如下方式挂载:

    from google.colab import drive
    drive.mount('/content/drive')
    

    注意

    每次使用新的协作者时,都要将驱动器挂载到所需的文件夹。

  4. 在您第一次挂载驱动器后,您需要通过点击 Google 提供的 URL 并输入授权代码。

  5. 现在您已经挂载了驱动器,接下来需要设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson5/Dataset
    

    注意

    步骤 5 中提到的路径可能会根据您在 Google Drive 上的文件夹设置而有所变化。路径将始终以cd /content/drive/My Drive/开头。

  6. 首先,让我们使用之前已经使用过的这两种方法从磁盘加载数据:

    import re, os, cv2
    import numpy as np
    rows,cols = 128,128
    //{…}##the detailed code can be found on Github##
    def list_files(directory, ext=None):
    //{…}##the detailed code can be found on Github##
    def load_images(path,label):
    //{…}
        for fname in list_files( path, ext='jpg' ): 
            img = cv2.imread(fname)
            img = cv2.resize(img, (rows, cols))
    //{…}##the detailed code can be found on Github##
    

    注意

    图像的大小设置为 128x128。这比之前使用的大小要大,因为我们需要图像中更多的细节,因为这些类别更难以区分,且主题呈现出不同的姿势,这使得工作变得更加困难。

  7. 我们加载相应的狗和猫的图像作为X(图像)和y(标签),并打印它们的形状:

    X, y = load_images('Dataset/dogs-cats/dogs',0)
    X_aux, y_aux = load_images('Dataset/dogs-cats/cats',1)
    X = np.concatenate((X, X_aux), axis=0)
    y = np.concatenate((y, y_aux), axis=0)
    print(X.shape)
    print(y.shape)
    

    图 5.12:狗猫数据形状

    图 5.12:狗猫数据形状
  8. 现在我们将导入random,设置种子,并显示一些数据样本:

    import random 
    random.seed(42) 
    from matplotlib import pyplot as plt
    
    for idx in range(5): 
        rnd_index = random.randint(0, X.shape[0]-1)
        plt.subplot(1,5,idx+1)
        plt.imshow(cv2.cvtColor(X[rnd_index],cv2.COLOR_BGR2RGB)) 
        plt.xticks([]),plt.yticks([])
    plt.show() 
    

    图 5.13:牛津宠物数据集的图像样本

    图 5.13:牛津宠物数据集的图像样本
  9. 为了预处理数据,我们将使用与Exercise 17: 构建 CNN中相同的过程:

    from keras import utils as np_utils
    X = (X.astype(np.float32))/255.0
    X = X.reshape(X.shape[0], rows, cols, 3) 
    y = np_utils.to_categorical(y, 2)
    input_shape = X.shape[1:]
    
  10. 现在,我们将Xy分别拆分为x_trainy_train(训练集),以及x_testy_test(测试集),并打印它们的形状:

    from sklearn.model_selection import train_test_split
    x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    print(x_train.shape)
    print(y_train.shape)
    print(x_test.shape)
    print(y_test.shape)
    

    图 5.14:训练集和测试集形状

    图 5.14:训练集和测试集形状
  11. 我们导入相应的数据以构建、编译和训练模型:

    from keras.models import Sequential
    from keras.callbacks import EarlyStopping, ModelCheckpoint
    from keras.layers import Input, Dense, Dropout, Flatten
    from keras.layers import Conv2D, Activation, BatchNormalization
    
  12. 让我们构建模型:

    def CNN(input_shape):
        model = Sequential()
    
        model.add(Conv2D(16, kernel_size=(5, 5), padding='same', strides=(2,2), input_shape=input_shape))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Conv2D(16, kernel_size=(3, 3), padding='same', strides=(2,2)))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
    //{…}##the detailed code can be found on Github##
    
        model.add(Conv2D(128, kernel_size=(2, 2), padding='same', strides=(2,2)))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.2))
    
        model.add(Flatten())
        model.add(Dense(512))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.5))
    
        model.add(Dense(2, activation="softmax"))
    
        return model
    

    该模型的第一个层有 16 个滤波器,最后一个层有 128 个滤波器,每 2 层大小翻一倍。

    由于这个问题更难(我们有更大的图像,且图像具有 3 个通道,尺寸为 128x128),我们使模型更深,添加了另外几层,在开始时使用 16 个滤波器(第一层使用 5x5 的卷积核,这在最初阶段更为有效),并在模型的最后再添加了几层 128 个滤波器。

  13. 现在,让我们编译模型:

    model = CNN(input_shape)
    model.compile(loss='categorical_crossentropy', optimizer='Adadelta', metrics=['accuracy'])
    ckpt = ModelCheckpoint('Models/model_dogs-cats.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False) 
    earlyStopping = EarlyStopping(monitor='val_loss', patience=15, verbose=0,mode='min')
    

    我们将EarlyStopping回调的耐心值设置为 15 个周期,因为模型需要更多的周期才能收敛到最佳位置,而验证损失在此之前可能会波动很大。

  14. 然后,我们训练模型:

    model.fit(x_train, y_train,
              batch_size=8,
              epochs=100,
              verbose=1, 
              validation_data=(x_test, y_test),
              callbacks=[ckpt,earlyStopping]) 
    

    批量大小也较小,因为我们没有太多数据,但它可以轻松增加到 16。

  15. 然后,评估模型:

    from sklearn import metrics
    model.load_weights('Models/model_dogs-cats.h5')
    y_pred = model.predict(x_test, batch_size=8, verbose=0)
    y_pred = np.argmax(y_pred, axis=1)
    y_test_aux = y_test.copy()
    y_test_pred = list()
    for i in y_test_aux:
        y_test_pred.append(np.argmax(i))
    
    print (y_pred)
    
    # Evaluate the prediction
    accuracy = metrics.accuracy_score(y_test_pred, y_pred)
    precision, recall, f1, support = metrics.precision_recall_fscore_support(y_test_pred, y_pred, average=None)
    print('\nFinal results...')
    print(metrics.classification_report(y_test_pred, y_pred))
    print('Acc      : %.4f' % accuracy)
    print('Precision: %.4f' % np.average(precision))
    print('Recall   : %.4f' % np.average(recall))
    print('F1       : %.4f' % np.average(f1))
    print('Support  :', np.sum(support))
    

    你应该看到以下输出:

    图 5.15:展示模型准确度的输出

    如前图所示,使用此模型在该数据集上实现的准确率为67.25%

  16. 我们将对该过程应用数据增强。我们需要从 Keras 导入 ImageDataGenerator,并声明它与我们将进行的转换:

    from keras.preprocessing.image import ImageDataGenerator
    datagen = ImageDataGenerator(
            rotation_range=15,
            width_shift_range=0.2,
            height_shift_range=0.2,
            horizontal_flip=True,
            zoom_range=0.3
            )
    

    应用了以下转换:

    我们设置了 15 度的旋转范围,因为图像中的狗和猫可能以稍微不同的方式呈现(可以根据需要调整此参数)。

    我们将宽度平移范围和高度平移范围设置为 0.2,以便在水平和垂直方向上平移图像,因为动物可能出现在图像的任何位置(同样可以调整)。

    我们将水平翻转属性设置为True,因为这些动物在数据集中可以水平翻转(对于垂直翻转,找到动物会更加困难)。

    最后,我们将缩放范围设置为 0.3,以便对图像进行随机缩放,因为图像中的狗和猫可能离得更远或更近。

  17. 我们将声明好的datagen实例与训练数据拟合,以计算特征标准化所需的量,并重新声明并编译模型,以确保不使用之前的模型:

    datagen.fit(x_train)
    
    model = CNN(input_shape)
    
    model.compile(loss='categorical_crossentropy', optimizer='Adadelta', metrics=['accuracy'])
    ckpt = ModelCheckpoint('Models/model_dogs-cats.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False)
    
  18. 最后,我们使用模型的fit_generator方法和datagen实例生成的flow()方法来训练模型:

    model.fit_generator(
              datagen.flow(x_train, y_train, batch_size=8),
              epochs=100,
              verbose=1, 
              validation_data=(x_test, y_test),
              callbacks=[ckpt,earlyStopping],
              steps_per_epoch=len(x_train) // 8,
              workers=4) 
    

    我们将steps_per_epoch参数设置为训练集长度除以批量大小(8)。

    我们还将工作线程数设置为 4,以便充分利用处理器的四个核心:

    from sklearn import metrics
    # Make a prediction
    print ("Making predictions...")
    model.load_weights('Models/model_dogs-cats.h5')
    #y_pred = model.predict(x_test)
    y_pred = model.predict(x_test, batch_size=8, verbose=0)
    y_pred = np.argmax(y_pred, axis=1)
    y_test_aux = y_test.copy()
    y_test_pred = list()
    for i in y_test_aux:
        y_test_pred.append(np.argmax(i))
    print (y_pred)
    # Evaluate the prediction
    accuracy = metrics.accuracy_score(y_test_pred, y_pred)
    precision, recall, f1, support = metrics.precision_recall_fscore_support(y_test_pred, y_pred, average=None)
    print('\nFinal results...')
    print(metrics.classification_report(y_test_pred, y_pred))
    print('Acc      : %.4f' % accuracy)
    print('Precision: %.4f' % np.average(precision))
    print('Recall   : %.4f' % np.average(recall))
    print('F1       : %.4f' % np.average(f1))
    print('Support  :', np.sum(support))
    

    你应该看到以下输出:

    图 5.16:展示模型准确度的输出

    图 5.16:展示模型准确度的输出

    如前图所示,经过数据增强后,我们实现了81%的准确率,效果远好于之前。

  19. 如果我们想加载我们刚刚训练的模型(狗与猫),以下代码可以实现这一点:

    from keras.models import load_model
    model = load_model('Models/model_dogs-cats.h5')
    
  20. 让我们用未见过的数据来试试这个模型。数据可以在Dataset/testing文件夹中找到,代码来自练习 17构建卷积神经网络(CNN)(但样本的名称不同):

    images = ['dog1.jpg', 'dog2.jpg', 'cat1.jpg', 'cat2.jpg'] 
    
    for number in range(len(images)):
        imgLoaded = cv2.imread('testing/%s'%(images[number])) 
        img = cv2.resize(imgLoaded, (rows, cols)) 
        img = (img.astype(np.float32))/255.0 
        img = img.reshape(1, rows, cols, 3) 
    
    

    在这些代码行中,我们加载了一张图片,将其调整为预期的大小(128 x 128),对图片进行了归一化处理——就像我们对训练集所做的那样——并将其重塑为(1, 128, 128, 3)的形状,以适应神经网络的输入。

    我们继续执行 for 循环:

      plt.subplot(1,5,number+1),plt.imshow(cv2.cvtColor(imgLoad ed,cv2.COLOR_BGR2RGB))
        plt.title(np.argmax(model.predict(img)[0])) 
        plt.xticks([]),plt.yticks([]) 
    fig = plt.gcf()
    plt.show()
    

图 5.17:使用卷积神经网络(CNNs)和数据增强对牛津宠物数据集进行预测,数据为未见过的数据

我们可以看到模型已经做出了所有正确的预测。请注意,并非所有的品种都存储在数据集中,因此并非所有猫狗都会被正确预测。为了实现这一点,需要添加更多品种类型。

活动 5:利用数据增强正确分类花卉图像

在此活动中,您将把所学内容付诸实践。我们将使用一个不同的数据集,其中图像更大(150x150)。该数据集包含 5 个类别:雏菊、蒲公英、玫瑰、向日葵和郁金香。该数据集总共有 4,323 张图像,较我们之前进行的练习要少。各个类别的图像数量也不相同,但请不要担心。图像是 RGB 格式的,因此将有三个通道。我们已将它们存储在每个类别的 NumPy 数组中,因此我们将提供一种方法来正确加载它们。

以下步骤将指导您完成此过程:

  1. 使用以下代码加载数据集,因为数据以 NumPy 格式存储:

    import numpy as np
    classes = ['daisy','dandelion','rose','sunflower','tulip']
    X = np.load("Dataset/flowers/%s_x.npy"%(classes[0]))
    y = np.load("Dataset/flowers/%s_y.npy"%(classes[0]))
    print(X.shape)
    for flower in classes[1:]:
        X_aux = np.load("Dataset/flowers/%s_x.npy"%(flower))
        y_aux = np.load("Dataset/flowers/%s_y.npy"%(flower))
        print(X_aux.shape)
        X = np.concatenate((X, X_aux), axis=0)
        y = np.concatenate((y, y_aux), axis=0)
    print(X.shape)
    print(y.shape)
    
  2. 通过导入randommatplotlib,并使用随机索引访问X集来显示数据集中的一些样本。

    注意

    NumPy 数组是以 BGR 格式存储的(OpenCV 格式),因此为了正确显示图像,您需要使用以下代码将格式转换为 RGB(仅用于显示图像):image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)

    您需要导入cv2

  3. X集进行归一化,并将标签设置为类别(即y集)。

  4. 将数据集分成训练集和测试集。

  5. 构建一个 CNN。

    注意

    由于图像较大,您应该考虑添加更多的层,从而减少图像的大小,且第一层应包含更大的卷积核(如果卷积核大于 3,则应为奇数)。

  6. 从 Keras 中声明 ImageDataGenerator,并根据您认为适合数据集变化的方式进行调整。

  7. 训练模型。您可以选择提前停止(EarlyStopping)策略,或设置较大的 epoch 数量,等待或随时停止它。如果您声明了 Checkpoint 回调,它将始终仅保存最佳的验证损失模型(如果您使用的是该指标)。

  8. 使用以下代码评估模型:

    from sklearn import metrics
    y_pred = model.predict(x_test, batch_size=batch_size, verbose=0)
    y_pred = np.argmax(y_pred, axis=1)
    y_test_aux = y_test.copy()
    y_test_pred = list()
    for i in y_test_aux:
        y_test_pred.append(np.argmax(i))
    accuracy = metrics.accuracy_score(y_test_pred, y_pred)
    print(accuracy)
    

    注意

    这将打印模型的准确率。请注意,batch_size 是您为训练集、x_testy_test 设置的批量大小,它们是您的测试集。

    您可以使用以下代码来评估任何模型,但首先需要加载模型。如果您要从.h5文件加载整个模型,则必须使用以下代码:

    from keras.models import load_modelmodel = load_model('model.h5')

  9. 尝试用未见过的数据测试模型。在 Dataset/testing/ 文件夹中,您将找到五张花卉图像,可以加载它们进行测试。请记住,类别的顺序如下:

    classes=['daisy','dandelion','rose','sunflower','tulip']

    所以,结果应如下所示:

图 5.18:使用 CNN 预测玫瑰花

图 5.18:使用 CNN 预测玫瑰花

注意

本活动的解决方案可以在第 313 页找到。

最先进的模型 - 迁移学习

人类并不是从零开始学习每一个想要实现的任务;他们通常会以之前的知识为基础,以便更快速地学习新任务。

在训练神经网络时,有些任务对于每个个体来说是非常昂贵的,例如需要数十万张图像进行训练,且必须区分两个或多个相似的物体,最终可能需要几天时间才能获得好的性能。这些神经网络经过训练以完成这个昂贵的任务,而由于神经网络能够保存这些知识,其他模型可以利用这些权重来重新训练特定的模型以执行类似的任务。

迁移学习正是做了这件事——它将预训练模型的知识转移到你的模型中,这样你就可以利用这些知识。

例如,如果你想创建一个能够识别五个物体的分类器,但这个任务训练起来似乎成本太高(需要知识和时间),你可以利用一个预训练的模型(通常是在著名的ImageNet数据集上训练的),并重新训练模型,使其适应你的问题。ImageNet 数据集是一个大型视觉数据库,旨在用于视觉物体识别研究,包含超过 1400 万张图像和超过 20000 个类别,个人训练起来是非常昂贵的。

从技术上讲,你加载模型时会使用该数据集的权重,如果你想解决不同的问题,你只需更改模型的最后一层。如果模型是在 ImageNet 上训练的,它可能有 1000 个类别,但你只有 5 个类别,所以你需要将最后一层更改为一个只有 5 个神经元的全连接层。你还可以在最后一层之前添加更多的层。

你导入的模型层(基础模型)可以被冻结,这样它们的权重就不会影响训练时间。根据这一点,迁移学习有两种类型:

  • 传统:冻结基础模型的所有层

  • 微调:仅冻结基础模型的一部分,通常是前几层

在 Keras 中,我们可以导入一些著名的预训练模型,如 Resnet50 和 VGG16。你可以选择导入带或不带权重的预训练模型(在 Keras 中,只有 ImageNet 的权重可用),并决定是否包含模型的顶部部分。输入形状只有在不包含顶部部分时才能指定,且最小大小为 32。

使用以下代码行,你可以导入 Resnet50 模型,不包括顶部部分,带有imagenet权重,并且输入形状为 150x150x3:

from keras.applications import resnet50
model = resnet50.ResNet50(include_top=False, weights='imagenet', input_shape=(150,150,3))

如果你已经包含了模型的顶部部分,因为你想使用模型的最后一层全连接层(假设你的问题类似于 ImageNet 但有不同的类别),那么你应该编写以下代码:

from keras.models import Model
from keras.layers import Dense

model.layers.pop()
model.outputs = [model.layers[-1].output]
model.layers[-1].outbound_nodes = []

x=Dense(5, activation='softmax')(model.output)
model=Model(model.input,x)

这段代码移除了分类层(最后的全连接层),并准备模型,以便你可以添加自己的最后一层。当然,你也可以在最后添加更多层,之后再添加分类层。

如果你没有添加模型的顶部层,你应该使用以下代码添加自己的顶部层:

from keras.models import Model
from keras.layers import Dense, GlobalAveragePooling2D
x=base_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(512,activation='relu')(x) #dense layer 2
x=Dropout(0.3)(x)
x=Dense(512,activation='relu')(x) #dense layer 3
x=Dropout(0.3)(x)
preds=Dense(5,activation='softmax')(x) #final layer with softmax activation
model=Model(inputs=base_model.input,outputs=preds)

这里,GlobalAveragePooling2D类似于一种最大池化方法。

对于这类模型,你应该像训练这些模型的数据一样预处理数据(如果你正在使用权重)。Keras 有一个preprocess_input方法,针对每个模型都会这样做。例如,对于 ResNet50,应该是这样的:

from keras.applications.resnet50 import preprocess_input

你将图像数组传递给那个函数,然后你的数据就准备好用于训练了。

模型中的学习率是指模型向局部最小值转换的速度。通常,你无需担心这一点,但如果你正在重新训练神经网络,这个参数需要调整。当你重新训练神经网络时,你应该降低该参数的值,以免神经网络忘记已经学到的内容。这个参数是在声明优化器时调整的。你可以选择不调整这个参数,尽管模型可能永远不会收敛或出现过拟合。

通过这种方法,你可以用非常少的数据训练网络,并获得总体良好的结果,因为你利用了模型的权重。

你还可以将迁移学习与数据增强结合使用。

练习 19:使用迁移学习和极少数据分类€5 和€20 账单

这个问题是关于用非常少的数据区分€5 和€20 账单。每个类别我们有 30 张图片,远少于之前的练习中的数据量。我们将加载数据,声明预训练模型,然后通过数据增强声明数据的变化,并训练模型。之后,我们将检查模型在未见数据上的表现:

  1. 打开你的 Google Colab 界面。

    注意

    你需要将Dataset文件夹挂载到你的驱动器上,并相应地设置路径。

  2. 声明加载数据的函数:

    import re, os, cv2
    import numpy as np
    def list_files(directory, ext=None):
    //{…}
    ##the detailed code can be found on Github##
    
    def load_images(path,label):
    //{…}
    ##the detailed code can be found on Github##
        for fname in list_files( path, ext='jpg' ): 
            img = cv2.imread(fname)
            img = cv2.resize(img, (224, 224))
    //{…}
    ##the detailed code can be found on Github##
    

    请注意,数据被调整为 224x224 的大小。

  3. 数据存储在Dataset/money/中,其中有两个类别在子文件夹内。为了加载数据,你需要写出以下代码:

    X, y = load_images('Dataset/money/20',0)
    X_aux, y_aux = load_images('Dataset/money/5',1)
    X = np.concatenate((X, X_aux), axis=0)
    y = np.concatenate((y, y_aux), axis=0)
    print(X.shape)
    print(y.shape)
    

    €20 账单的标签是 0,€5 账单的标签是 1。

  4. 让我们展示数据:

    import random 
    random.seed(42) 
    from matplotlib import pyplot as plt
    
    for idx in range(5): 
        rnd_index = random.randint(0, 59)
        plt.subplot(1,5,idx+1),plt.imshow(cv2.cvtColor(X[rnd_index],cv2.COLOR_BGR2RGB)) 
        plt.xticks([]),plt.yticks([])
    plt.savefig("money_samples.jpg", bbox_inches='tight')
    plt.show() 
    

    图 5.21:账单样本

    图 5.19:账单样本
  5. 现在我们将声明预训练模型:

    from keras.applications.mobilenet import MobileNet, preprocess_input
    from keras.layers import Input, GlobalAveragePooling2D, Dense, Dropout
    from keras.models import Model
    
    input_tensor = Input(shape=(224, 224, 3))
    
    base_model = MobileNet(input_tensor=input_tensor,weights='imagenet',include_top=False)
    
    x = base_model.output
    x = GlobalAveragePooling2D()(x)
    x = Dense(512,activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(2, activation='softmax')(x)
    
    model = Model(base_model.input, x)
    

    在这个例子中,我们正在加载 MobileNet 模型,并使用 imagenet 的权重。我们没有包括顶部层,因此我们应该构建自己的顶部层。输入形状是 224x224x3。

    我们通过获取 MobileNet 最后一层(非分类层)的输出,开始构建模型的顶部。我们添加了GlobalAveragePooling2D用于图像压缩,一个我们可以针对特定问题训练的密集层,一个Dropout层以避免过拟合,最后是分类层。

    最后的密集层有两个神经元,因为我们只有两个类别,并且使用了Softmax激活函数。对于二分类问题,也可以使用 Sigmoid 函数,但这会改变整个过程,因为不应该将标签做成类别形式,并且预测结果也会有所不同。

    然后,我们创建了一个模型,输入为 MobileNet,输出为分类密集层。

  6. 我们将进行微调。为了做到这一点,我们必须冻结一些输入层,并保持其余可训练的数据不变:

    for layer in model.layers[:20]:
        layer.trainable=False
    for layer in model.layers[20:]:
        layer.trainable=True
    
  7. 让我们使用Adadelta优化器编译模型:

    import keras
    model.compile(loss='categorical_crossentropy',optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'])
    
  8. 现在我们将使用之前导入的preprocess_input方法对 MobileNet 的X集进行预处理,然后将标签y转换为独热编码:

    from keras import utils as np_utils
    X = preprocess_input(X)
    #X = (X.astype(np.float32))/255.0 
    y = np_utils.to_categorical(y)
    
  9. 我们使用train_test_split方法将数据集分割为训练集和测试集:

    from sklearn.model_selection import train_test_split
    x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    print(x_train.shape)
    print(y_train.shape)
    print(x_test.shape)
    print(y_test.shape)
    
  10. 我们将对数据集应用数据增强:

    from keras.preprocessing.image import ImageDataGenerator
    train_datagen = ImageDataGenerator(  
          rotation_range=90,     
          width_shift_range = 0.2,
          height_shift_range = 0.2,
          horizontal_flip=True,    
          vertical_flip=True,
          zoom_range=0.4)
    train_datagen.fit(x_train)
    

    由于账单可能处于不同角度,我们选择设置旋转范围为 90º。其他参数对于此任务来说似乎合理。

  11. 我们声明一个检查点,当验证损失减少时保存模型,并训练该模型:

    from keras.callbacks import ModelCheckpoint
    ckpt = ModelCheckpoint('Models/model_money.h5', save_best_only=True, monitor='val_loss', mode='min', save_weights_only=False)
    model.fit_generator(train_datagen.flow(x_train, y_train,
                                    batch_size=4),
                        epochs=50,
                        validation_data=(x_test, y_test),
                        callbacks=[ckpt],
                        steps_per_epoch=len(x_train) // 4,
                        workers=4)
    

    我们将批次大小设置为 4,因为我们只有少量数据,并且不希望将所有样本一次性传递给神经网络,而是分批处理。由于数据不足,并且使用 Adadelta 优化器时学习率较高,所以我们没有使用 EarlyStopping 回调函数,因为损失值会上下波动。

  12. 检查结果:图 5.22:显示期望的输出

    图 5.20:显示期望的输出

    在上图中,我们可以看到,在第 7 个周期时,模型已经达到了 100%的准确率,并且损失较低。这是由于验证集数据的不足,因为只有 12 个样本,无法判断模型在未见数据上的表现。

  13. 让我们运行代码来计算该模型的准确率:

    y_pred = model.predict(x_test, batch_size=4, verbose=0)
    y_pred = np.argmax(y_pred, axis=1)
    y_test_aux = y_test.copy()
    y_test_pred = list()
    for i in y_test_aux:
        y_test_pred.append(np.argmax(i))
    
    accuracy = metrics.accuracy_score(y_test_pred, y_pred)
    print('Acc: %.4f' % accuracy)
    

    输出如下:

    图 5.23:达成 100%准确率

    图 5.21:达成 100%准确率
  14. 让我们使用新数据测试这个模型。Dataset/testing文件夹中有测试图像。我们添加了四个账单示例来检查模型是否能准确预测:

    注意

    images = ['20.jpg','20_1.jpg','5.jpg','5_1.jpg']
    model.load_weights('Models/model_money.h5')
    for number in range(len(images)):
        imgLoaded = cv2.imread('Dataset/testing/%s'%(images[number])) 
        img = cv2.resize(imgLoaded, (224, 224)) 
        #cv2.imwrite('test.jpg',img) 
        img = (img.astype(np.float32))/255.0 
        img = img.reshape(1, 224, 224, 3) 
        plt.subplot(1,5,number+1),plt.imshow(cv2.cvtColor(imgLoaded,cv2.COLOR_BGR2RGB)) 
        plt.title('20' if np.argmax(model.predict(img)[0]) == 0 else '5') 
        plt.xticks([]),plt.yticks([]) 
    plt.show()
    

    在这段代码中,我们也加载了未见过的示例,并且我们将输出图像合并,结果如下所示:

图 5.24:账单预测

图 5.22:账单预测

模型已精确预测所有图像!

恭喜!现在你可以在数据量较少的情况下,借助迁移学习训练一个自己的模型了。

注意

这个练习的完整代码已上传到 GitHub:https://github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson05/Exercise19/Exercise19.ipynb。

总结

CNN 在处理图像时的表现明显优于全连接神经网络。此外,CNN 也能够在处理文本和声音数据时取得良好结果。

CNN 已经被深入讲解,并且卷积的工作原理以及相关的所有参数也得到了详细说明。之后,所有这些理论通过一个练习进行了实践。

数据增强是一种通过对原始数据进行简单的转换来生成新图像,从而克服数据不足或数据集变异性不足的技术。此技术已经通过一个练习和活动进行了说明和实践,在其中你可以实验所学到的知识。

迁移学习是一种在数据不足或问题过于复杂,以至于使用常规神经网络训练会花费太长时间时使用的技术。此外,这种技术对神经网络的理解要求较低,因为模型已经实现。它也可以与数据增强结合使用。

迁移学习也已被涵盖,并通过一个练习进行实践,其中的数据量非常小。

学习如何构建 CNN 对于计算机视觉中的物体或环境识别非常有用。当机器人使用视觉传感器识别环境时,通常会使用 CNN,并通过数据增强来提高 CNN 的性能。在第八章使用 CNN 进行物体识别以引导机器人中,你将把所学的 CNN 概念应用到实际应用中,并能够利用深度学习识别环境。

在应用这些技术来识别环境之前,首先你需要学习如何管理一个能够识别环境的机器人。在第六章,机器人操作系统(ROS)中,你将通过利用名为 ROS 的软件,学习如何使用模拟器来管理机器人。

第七章:第六章

机器人操作系统(ROS)

学习目标

到本章结束时,你将能够:

  • 解释机器人操作系统(ROS)的基本概念和要点

  • 创建机器人操作系统软件包并与之协作

  • 使用从传感器获取的信息操作虚拟机器人

  • 开发并实现机器人的工作程序

本章重点介绍 ROS 及其软件包的不同工作方式。你还将学习如何根据从传感器接收到的信息使用 ROS 操作虚拟机器人。

介绍

为机器人开发软件并不像为其他类型的软件开发那么简单。要构建机器人,你需要一些方法和功能,这些方法能够让你访问传感器信息、控制机器人部件并与机器人连接。这些方法和功能都包含在 ROS 中,使得构建虚拟机器人变得更容易。

ROS 是一个与 Ubuntu(Linux)兼容的框架,用于编写机器人软件。它是一组库和工具,通过这些工具可以构建和创建各种机器人行为。关于这个框架最有趣的特点之一是,开发的代码可以适配任何其他机器人。ROS 还允许你在多台机器上同时工作;例如,如果你想用机器人采摘苹果,你可以使用一台计算机获取苹果的摄像头信息并进行处理,另一台机器发出控制机器人的运动命令,最后机器人将摘取苹果。通过这种工作流程,计算机不会执行过多的计算任务,执行过程也更加流畅。

ROS 是机器人领域最广泛使用的工具,既适用于研究人员,也适用于公司。它正成为机器人任务的标准工具。此外,ROS 正在不断发展,以解决新的问题,并适应不同的技术。所有这些事实使它成为学习和实践的好主题。

ROS 概念

如前所述,第一次使用 ROS 时并不容易。但就像任何其他软件一样,你需要了解 ROS 是如何工作的,并学会如何使用它执行某些任务。在安装或使用框架之前,理解其基本概念是非常重要的。ROS 功能背后的关键思想将帮助你理解其内部流程,下面列出了这些关键点:

  • 节点:ROS 节点是负责执行任务和计算的进程。它们可以通过话题或其他更复杂的工具相互组合。

  • 话题:话题可以定义为节点之间的信息通道,工作方式是单向的。这被视为一种单向工作流,因为节点可以订阅话题,但话题并不知道哪些节点已订阅它。

  • Master:ROS 主控是一个提供节点名称和注册的服务。它的主要功能是启用各个节点,使它们能够相互定位并建立点对点通信。

  • Package:包是 ROS 组织的核心。在这些包中,你可以找到节点、库、数据集或用于构建机器人应用程序的有用组件。

  • Stack:一个 ROS 栈是一组节点,它们共同提供某些功能。当需要开发的功能过于复杂时,将任务分配给不同的节点可能会非常有用。

除了上述概念外,还有许多其他概念在使用 ROS 时也非常有用,但理解这些基础概念将使你能够为机器人实现强大的程序。让我们来看一个简单的例子,了解它们在实际情况中的使用方法:

图 6.1:使用 ROS 的真实示例系统的框图

图 6.1:使用 ROS 的真实示例系统的框图

这里的情况是,当机器人检测到近距离障碍物时,它会改变方向。其工作原理如下:

  1. ROS 主控已启用。这意味着 ROS 系统已启动,可以运行任何节点。

  2. 接近节点启动并提取激光传感器的信息。它会通知主控发布这些获取的信息。如果没有问题,并且信息类型符合预期,主控将允许节点通过话题发布。

  3. 一旦主控允许节点发布,信息会被传递到一个话题并发布。在这种情况下,接近节点在激光话题中发布信息。

  4. 移动节点请求主控订阅激光话题。订阅后,它将获得发布的信息,并使用这些信息决定机器人执行的下一个动作。

总结来说,两个节点可以通过主控服务共享信息,该服务会通知两个节点彼此的存在。

ROS 命令

ROS 没有图形用户界面来进行操作;每个操作必须在命令行中执行,因为它与 Ubuntu 兼容。然而,在使用 ROS 之前,你需要了解它最常用的命令。这里有一个简短的命令列表以及它们的功能:

  • roscore:这是与 ROS 一起工作时要运行的第一个命令。它启用框架并为任何 ROS 程序或操作提供支持。必须启动它才能允许节点间的通信。

  • roscd:此命令用于切换到一个栈或包目录,而无需进入物理路径。

  • rosnode:这些命令用于管理节点并获取关于它们的信息。在这里,你可以看到最常用的 rosnode 命令:

  • rosnode list:此命令打印活动节点的信息。

  • rosnode info <node_name>:此命令用于通知用户关于输入节点的信息。

  • rosnode kill <node_name>:此命令的功能是停止一个节点进程。

  • rosrun:使用此命令,你可以运行系统上的任何应用程序,而无需切换到其目录。

  • rostopic:此命令允许你管理和检查主题信息。此命令还有多个其他类型:

  • rostopic list:此命令打印活动主题的信息。

  • rostopic info <topic_name>:此命令显示关于具体主题的信息。

  • rostopic pub <topic_name> [data...]:此命令的功能是将给定的数据发布到指定的主题。

  • rqt_graph:这是一个非常有用的命令,可以用来图形化地观察活动节点和正在发布或订阅的主题。

安装与配置

安装 ROS 之前,首先需要考虑安装的 Ubuntu 版本。根据你的操作系统版本,有几个 ROS 版本可供选择。在这种情况下,我们正在解释安装兼容 Ubuntu 16.04 LTS(Xenial Xerus)的 ROS Kinetic Kame 版本。

注意

如果这不是你的 Ubuntu 版本,你可以前往 ROS 官网,www.ros.org/,查找对应的发行版。

就像几乎所有其他工具一样,建议始终安装最新版本,因为它可能已经解决了错误或新增了功能;但如前所述,如果你使用的是旧版本,也不必担心。

注意

要获取安装 ROS 的详细步骤,请参阅前言第 vi 页。

Catkin 工作空间和包

这是在为机器人编写第一个应用程序和程序之前的最后一步。你需要设置工作环境。为此,你将学习什么是 catkin 工作空间和包,以及如何使用它们。

catkin 工作空间是一个 ROS 目录,可以在其中创建、编译和运行 catkin 包。catkin 包是用于创建 ROS 节点和应用程序的容器。每个包作为一个单独的项目,可以包含多个节点。需要了解的是,catkin 包中的 ROS 代码只能是 Python 或 C++语言。

现在,让我们来看一下如何创建 catkin 工作空间:

注意

在同一终端窗口中执行这些命令。

  1. 创建一个标准文件夹,其中包含一个名为"src"的子文件夹。你可以选择系统上的任何位置:

    mkdir -p ~/catkin_ws/src
    cd ~/catkin_ws
    
  2. 切换到新的catkin_ws目录并运行catkin编译命令来初始化新的工作空间:

    catkin_make
    

    每次在任何包中进行更改时,必须执行此命令才能编译工作空间。

通过遵循这些简单的步骤,你将完成 catkin 工作空间的设置。但在使用时,你应始终记住先输入以下命令:

source devel/setup.bash

这让 ROS 知道在创建的 catkin 工作空间中可以存在 ROS 可执行文件。

如果你已经成功完成了前面的过程,现在可以创建 catkin 包并开始工作。按照此处提到的步骤创建一个包:

  1. 进入你的 catkin 工作空间中的"src"文件夹:

    cd ~/catkin_ws/src
    
  2. 使用此命令创建一个包:

    catkin_create_pkg <package_name> [dependencies]
    

依赖项是包正常运行所需的一组库或工具。例如,在一个仅使用 Python 代码的简单包中,命令将如下所示:

catkin_create_pkg my_python_pkg rospy

发布者和订阅者

在解释基本的 ROS 概念时,我们讨论了一些用于发布数据的节点和一些用于订阅数据的节点。了解这一点后,我们不难想象,节点可以根据它们执行的操作类型分为两类。它们可以是发布者订阅者。你认为区分这两种类型的节点为什么很重要?

如前所述,发布者是向其他节点提供信息的节点。它们通常与传感器一起工作,检查环境状态并将其转换为有价值的输出,供能够接收这些信息的订阅者使用。

另一方面,订阅者通常会接收可以理解的输入并进行处理。然后,它们会根据获得的结果决定将启动哪个操作。

由于这是一种较少使用的编程类型,在开始将其应用于机器人和模拟器之前,跟随一些示例了解这些节点的实际工作方式将会很有趣。所以,让我们通过一些练习来帮助你理解节点的工作原理。

练习 20:发布与订阅

在这个例子中,我们将使用以下步骤编写一个简单的发布者和订阅者:

  1. 打开一个新的终端并输入roscore命令以启动 ROS 服务:

    roscore
    
  2. 在你的 catkin 工作空间中创建一个新包,其中包含该练习的解决方案。此包将依赖于rospystd_msgs,因此你必须按如下方式创建它:

    catkin_create_pkg exercise20 rospy std_msgs
    

    注意

    std_msgs是一个为 ROS 原始数据类型提供支持的包。你可以在这里找到更多有关它的信息,包括管理数据的具体类型:wiki.ros.org/std_msgs

  3. 切换到包目录并创建一个新文件夹,其中将包含发布者和订阅者文件,例如:

    cd ~/catkin_ws/src/exercise20
    mkdir –p scripts
    
  4. 进入新文件夹并为每个节点创建相应的 Python 文件:

    cd scripts
    touch publisher.py
    touch subscriber.py
    
  5. 为两个文件提供可执行权限:

    chmod +x publisher.py
    chmod +x subscriber.py
    
  6. 从发布者实现开始:

    初始化 Python 环境并导入必要的库。

    注意

    #!/usr/bin/env python
    import rospy
    from std_msgs.msg import String
    

    创建一个函数来发布消息。

    def publisher():
    

    声明一个发布者,将一个String消息发布到一个新主题,无论其名称是什么。

        pub  =rospy.Publisher('publisher_topic', String, queue_size=1)
    

    注意

        rospy.init_node('publisher', anonymous=True)
    

    使用创建的发布者变量发布任何所需的String。例如:

        pub.publish("Sending message")
    

    最后,检测程序入口并调用创建的函数:

    if __name__ == '__main__':
        publisher()
    
  7. 继续实现订阅者:

    初始化 Python 并导入库,方法与为发布者所做的一样。

    注意

    #!/usr/bin/env python
    import rospy
    from std_msgs.msg import String
    

    创建一个函数来订阅该主题:

    def subscriber():
    

    以与你之前相同的方式初始化节点:

        rospy.init_node('subscriber', anonymous=True)
    

    使用此函数订阅publisher_topic

        rospy.Subscriber('publisher_topic', String, callback)
    

    注意

        rospy.spin()
    

    实现callback函数,当接收到任何数据时打印消息。对于这个第一个练习,我们将在接收到来自发布者的第一条消息时关闭订阅者节点。这可以通过signal_shutdown方法实现,该方法集成在rospy中,只需要一个关闭理由作为参数:

    def callback(data):
        if(data != None):
            print("Message received")
            rospy.signal_shutdown("Message received")
    

    从主执行线程调用创建的函数:

        if __name__ == '__main__':
            subscriber()
    
  8. 测试创建的节点功能。你可以按照这里描述的方式进行测试:

    打开一个新的终端并切换到工作区。然后,运行以下命令,让 ROS 检查是否有可执行文件:

    source devel/setup.bash
    

    运行订阅者节点。如果实现正确,它应该保持运行,直到你启动发布者:

    rosrun exercise20 subscriber.py
    

    打开一个新终端并再次输入命令。

    运行发布者节点:

    rosrun exercise20 publisher.py
    

    如果节点实现正确,订阅者执行完后会结束,输出必须是回调中打印的消息,在这种情况下是:Message received

    注意

    不需要编译工作区来运行你的包节点,因为它们是用 Python 编写的。如果是 C++编写的,你每次修改代码后都需要重新构建包。

练习 21:发布者与订阅者

这个练习与之前的类似,但更复杂。之前创建的发布者每次执行时只能发送一条消息。而现在,我们将实现一个发布者,直到我们终止它之前,它将不停地发送数据。

这个练习的目标是创建一个数字查找系统,遵循以下规则:

  • 发布者节点必须向主题发布随机数字,直到用户停止它。

  • 订阅者节点决定查找的数字,并在接收到的消息列表中查找它。这里有两种可能性:

    如果在 1000 次尝试之前找到数字,将打印一条正面消息,并显示达到目标所花费的尝试次数。

    如果在 1000 次尝试内未找到数字,将打印一条负面消息,告知用户无法找到该数字。

    所以,可以通过以下方式进行:

  1. 如前所述,从创建包和文件开始:

    cd ~/catkin_ws/src
    catkin_create_pkg exercise21 rospy std_msgs
    cd exercise21
    mkdir scripts
    cd scripts
    touch generator.py
    touch finder.py
    chmod +x generator.py finder.py
    
  2. 从发布者实现开始。

    导入必要的库。这些库与前言中的相同,但这次必须将String导入更改为Int32,因为节点将处理数字。你还应该导入一个随机库来生成数字。

    注意

    #!/usr/bin/env python
    import rospy
    from std_msgs.msg import Int32
    import random
    
  3. 创建数字生成器函数:

    def generate():
    
  4. 声明发布者并像之前的练习一样初始化节点。注意,这次数据类型不同,队列大小设置为 10,这意味着最多可以有 10 个已发布的数字。当第 11 个数字发布时,第一个数字会被从队列中丢弃:

        pub = rospy.Publisher('numbers_topic', Int32, queue_size=10)
        rospy.init_node('generator', anonymous=True)
    
  5. 配置程序循环的迭代频率。我们设置的频率为 10(Hz),这是一个不太高的频率,能够让我们检查生成的数字:

        rate = rospy.Rate(10)
    
  6. 实现生成和发布数字的循环。它必须迭代直到用户停止,因此你可以使用is_shutdown()函数。使用声明的速率上的 sleep 函数,以使其生效:

        while not rospy.is_shutdown():
            num = random.randint(1,101)
            pub.publish(num)
            rate.sleep()
    
  7. 从节点入口调用创建的函数。使用 try 指令,以确保用户关闭时不会产生错误:

    if __name__ == '__main__':
        try:
            generate()
        except rospy.ROSInterruptException:
            pass
    
  8. 继续进行订阅者的实现:

    导入必要的库。

    注意

    #!/usr/bin/env python
    import rospy
    from std_msgs.msg import Int32
    
  9. 创建一个具有两个属性的类:一个用于设置要查找的数字的值,另一个用于计算尝试的次数:

    class Finder:
        searched_number = 50
        generated_numbers = 0
    
  10. 实现回调函数。寻找器的逻辑必须在这个函数中编写。有很多方法可以做到这一点,但这是一个常用的方法:

        def callback(self, data):
            if data.data == self.searched_number:
                print(str(data.data) + ": YES")
                self.generated_numbers += 1
                print("The searched number has been found after " + str(self.generated_numbers) + " tries")
                rospy.signal_shutdown("Number found")
            elifself.generated_numbers>= 1000:
    print("It wasn't possible to find the searched number")
                rospy.signal_shutdown("Number not found")
    else:
                print(str(data.data) + ": NO")
                self.generated_numbers += 1
    

    如你所见,这是一个简单的函数,它寻找数字并为每次失败的尝试增加一次计数。如果找到数字,它将打印一个正面的消息。如果计数器达到 1000,搜索将中止,并显示负面消息。

  11. 创建订阅函数。记住,这次发布的数据类型是Int32

        def finder(self):
            rospy.init_node('finder', anonymous=True)
            rospy.Subscriber('numbers_topic', Int32, self.callback)
            rospy.spin()
    
  12. 最后,从节点入口创建一个Finder类实例,并调用finder方法:

    if __name__ == '__main__':
        find = Finder()
        find.finder()
    
  13. 测试执行的实现是否正确。

    打开一个新的终端并运行roscore

    打开另一个终端并执行订阅者节点:

    cd ~/catkin_ws
    source devel/setup.bash
    rosrun exercise21 finder.py
    
  14. 在另一个终端中,运行发布者节点,以便生成数字并开始执行回调函数:

    cd ~/catkin_ws
    source devel/setup.bash
    rosrun exercise21 generator.py
    
  15. 如果找到要搜索的数字,这里是 50,输出应类似于此:图 6.2:找到数字的执行示例

    图 6.2:找到数字的执行示例
  16. 当未找到数字时,将搜索的数字更改为大于 100 的值。你应该获得如下的输出:

图 6.3:未找到数字的执行示例

图 6.3:未找到数字的执行示例

当两个节点都在运行时,使用rqt_graph命令会很有意思;这样,你可以图形化地看到你刚刚创建的结构。所以,打开一个新的终端并输入命令。输出应该类似于以下内容:

图 6.4:输出

图 6.4:输出

仿真器

仿真器是开发和测试机器人软件的非常好工具。它们使机器人技术变得对每个人都能负担得起。想象一下你正在进行一个机器人项目,在这个项目中,你需要不断测试机器人功能的改进。这将需要每次测试时都要连接机器人,充电多次,还要带着它到处移动。所有这些都可以通过仿真器来避免,仿真器可以在任何时候在你的计算机上启动,甚至可以模拟机器人生成的节点和话题。你知道有什么机器人仿真器吗?

我们将使用 Gazebo,这是 ROS 完整安装中包含的一个模拟器。事实上,如果您在安装时选择了这个选项,您只需在终端输入“gazebo”即可启动模拟器。Gazebo 界面如图 6.4 所示:

图 6.5:Gazebo 启动点

图 6.5:Gazebo 启动点

下一步是安装并设置我们要模拟的机器人。在这个案例中,我们将使用 Turtlebot,它是一种配备有摄像头、激光传感器等的轮式机器人。Turtlebot 可能与您的 ROS 发行版(我们使用的是 Kinetic Kame)不兼容;但不用担心,Gazebo 中有许多机器人可以模拟。您可以查找不同的机器人并尝试与您的 ROS 发行版配合使用。

练习 22:Turtlebot 配置

在本练习中,我们将介绍一些在使用 Turtlebot 之前需要做的事情:

  1. 安装其依赖项:

    sudo apt-get installros-kinetic-turtlebotros-kinetic-turtlebot-apps ros-kinetic-turtlebot-interactions ros-kinetic-turtlebot-simulator ros-kinetic-kobuki-ftdiros-kinetic-ar-track-alvar-msgs
    
  2. 下载 Turtlebot 仿真包到您的 catkin 工作空间。

    注意

    cd ~/catkin_ws/src
    git clone https://github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson06/turtlebot_simulator.zip
    
  3. 之后,您应该能够与 Gazebo 一起使用 Turtlebot。

    启动 ROS 服务:

    roscore
    

    启动 Turtlebot 世界:

    cd ~/catkin_ws
    source devel/setup.bash
    roslaunch turtlebot_gazebo turtlebot_world.launch
    
  4. 现在,您应该会看到与之前相同的 Gazebo 世界,但在中心有一组对象,包括 Turtlebot,如图 6.5 所示:

图 6.6:使用 Gazebo 的 Turtlebot 仿真

图 6.6:使用 Gazebo 的 Turtlebot 仿真

一旦仿真成功运行,我们将进行另一个练习,学习如何从传感器获取信息并进行处理。

练习 23:模拟器与传感器

在本练习中,我们将创建一个 ROS 节点,订阅 Turtlebot 摄像头以获取相应的图像。请按照以下步骤进行操作:

  1. 创建一个包含必要依赖项和文件的软件包:

    cd ~/catkin_ws/src
    catkin_create_pkg exercise22 rospy sensor_msgs
    cd exercise22
    mkdir scripts
    cd scripts
    touch exercise22.py
    chmod +x exercise22.py
    
  2. 实现节点。

    导入必要的库。在本练习中,我们将使用 OpenCV 来处理从摄像头获取的图像:

    #!/usr/bin/env python
    import rospy
    from sensor_msgs.msg import Image
    import cv2
    from cv_bridge import CvBridge
    

    创建一个类并声明一个类型为 CvBridge 的属性,稍后将用它来将图像类型转换为 cv2

    class ObtainImage:
        bridge = CvBridge()
    

    编写回调函数,在该函数中,您需要获取图像并将其转换为 cv2 格式:

        def callback(self, data):
            cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
            cv2.imshow('Image',cv_image)
            cv2.waitKey(0)
            rospy.signal_shutdown("Finishing")
    

    注意

    我们使用 waitKey() 函数,这样图像就会保持在屏幕上,直到用户按下任意键,图像才会消失。

  3. 定义并实现订阅者函数。记住,现在,所需的数据已经是一个图像类型(Image):

        def obtain(self):
                rospy.Subscriber('/camera/rgb/image_raw', Image, self.callback)
                rospy.init_node('image_obtainer', anonymous=True)
                rospy.spin()
    

    注意

    如果您不知道想要订阅的主题名称,您可以随时输入 rostopic list 命令,查看可用的节点。您应该会看到如下列表:

    图 6.7: 命令的输出

    图 6.7:rostopic list 命令的输出
  4. 从程序入口处调用订阅者函数:

    if __name__ == '__main__':
        obt = ObtainImage()
        obt.obtain()
    
  5. 检查节点是否正常工作。为此,您需要在不同的终端中运行 roscore 命令、Gazebo 和 Turtlebot,同时创建节点。请注意,如果您之前没有这样做,也可以运行 source devel/setup.bash 命令:

    roscore
    roslaunch turtlebot_gazebo turtlebot_world.launch
    rosrun exercise22 exercise22.py
    

    结果应该类似于这样:

图 6.8:练习节点的执行示例

图 6.8:练习节点的执行示例

活动 6:模拟器和传感器

考虑以下情景:你为一家机器人公司工作,该公司最近获得了一位新客户——一家安防监控公司。因此,你被要求为一个守夜机器人实现一个监控系统。客户希望机器人停留在商店的中央,并不断四处查看。

你需要模拟这个系统,并且已经被要求使用 Turtlebot 和 Gazebo。

  1. 实现一个节点,订阅相机并显示它接收到的所有图像。

  2. 实现一个节点,使机器人能够自启动。

    注意

    为此,你需要发布 /mobile_base/commands/velocity 主题,该主题与 Twist 消息一起使用。Twist 是 geometry_msgs 库中的一种消息类型,因此你需要将其添加为依赖项。为了使机器人自转,创建一个 Twist 实例并修改其 angular.z 值,然后发布它。

  3. 现在,同时运行这两个节点。

    在这个活动的最后,你将得到类似于这样的输出:

图 6.9:展示虚拟环境中图像的旋转输出

图 6.9:展示虚拟环境中图像的旋转输出

注意

这个活动的解决方案可以在第 318 页找到。

总结

在本章中,你学习了如何使用 ROS,从其安装和配置到节点的实现。你还使用了模拟器及其传感器,从中获取信息,并将这些获取的信息应用于解决问题。本章中涵盖的所有练习和活动将在后续章节中对你有所帮助。

在下一章,你将学习自然语言处理(NPL)并学会如何构建聊天机器人。如果你能构建一个优秀的聊天机器人,它将成为一个非常有趣的工具,可以添加到机器人中。你甚至可以使用 ROS 来开发它。

第八章:第七章

构建基于文本的对话系统(聊天机器人)

学习目标

在本章结束时,您将能够:

  • 定义 GloVe、Word2Vec 和嵌入的术语

  • 开发你自己的 Word2Vec

  • 选择工具来创建对话代理

  • 预测对话的意图

  • 创建一个对话代理

本章介绍了 GloVe、Word2Vec、嵌入等术语以及将帮助你创建对话代理的工具。

介绍

深度自然语言处理(NLP)中的最新趋势之一是创建对话代理,也叫聊天机器人。聊天机器人是一个基于文本的对话系统,它能够理解人类语言,并能够与人进行真实的对话。许多公司使用这些系统与客户互动,获取信息和反馈,例如,对新产品发布的意见。

聊天机器人被用作助手,例如,Siri、Alexa 和 Google Home。这些助手可以提供实时的天气或交通信息。

目前的问题是,机器人如何理解我们?在前几章中,我们回顾了语言模型及其工作原理。然而,在语言模型(LMs)中,最重要的事情是单词在句子中的位置。每个单词在句子中出现的概率是有一定的,取决于句子中已经出现的单词。但概率分布方法并不适用于这个任务。在这种情况下,我们需要理解单词的意义,而不是预测下一个单词,之后模型将能够理解给定语料库中单词的意义。

一个单词本身没有意义,除非它被放在一个上下文或语料库中。理解一个句子的意义是很重要的,而这由句子的结构决定(也就是单词在句子中的位置)。模型将通过查看哪些单词接近它来预测单词的意义。但首先,如何用数学方式来表示这个呢?

第四章神经网络与自然语言处理》中,我们讨论了如何使用一个 one-hot 编码向量来表示一个单词,这个向量由 1 和 0 组成。然而,这种表示方式并没有提供单词的实际含义。我们来看一个例子:

  • 狗  [1,0,0,0,0,0]

  • 猫  [0,0,0,0,1,0]

狗和猫是动物,但它们在 1 和 0 中的表示并没有提供任何关于这些单词含义的信息。

但是,如果这些向量根据单词的含义为我们提供了两个单词之间的相似性会发生什么呢?具有相似含义的两个单词将被放置在平面上彼此靠近,而没有任何关系的单词则不会。例如,一个国家的名称和它的首都是相关的。

通过这种方法,一组句子可以与对话意图或特定主题相关联(也称为意图,这个术语将在本章中反复使用)。使用这种系统,我们将能够与人类进行合理的对话交流。

对话的意图是对话的主题。例如,如果你正在谈论皇家马德里与巴塞罗那的比赛,那么对话的意图就是足球。

在本章稍后,我们将回顾将单词表示为向量的基本概念,以及如何创建这样的向量并用它们来识别对话的意图。

向量空间中的词表示

本节将介绍从语料库中计算单词的连续向量表示的不同架构。这些表示将取决于单词在意义上的相似性。此外,还将介绍一个新的 Python 库 (Gensim) 来完成这项任务。

词嵌入

词嵌入是一种将语料库中的单词和句子映射并输出为向量或实数的技术和方法的集合。词嵌入通过表示单词出现的上下文来生成每个单词的表示。词嵌入的主要任务是将每个单词的空间维度从一个维度降到一个连续的向量空间。

为了更好地理解这是什么意思,让我们看一个例子。假设我们有两句话,它们相似,例如:

  • 我很好。

  • 我很好。

现在,将这些句子编码为独热向量,我们得到类似这样的表示:

  • 我  [1,0,0,0]

  • Am  [0,1,0,0]

  • Good  [0,0,1,0]

  • Great  [0,0,0,1]

我们知道前两个句子是相似的(在意义上),因为“great”和“good”有相似的意思。但我们如何衡量这两个单词的相似度呢?我们有两个向量表示这两个单词,那么让我们计算余弦相似度。

余弦相似度

余弦相似度衡量两个向量之间的相似度。顾名思义,这种方法将表示两个句子之间角度的余弦值。其公式如下:

图 7.1:余弦相似度公式

图 7.1:余弦相似度公式

图 7.1 展示了余弦相似度的公式。A 和 B 是向量。按照之前的示例,如果我们计算“good”和“great”之间的相似度,结果是 0。这是因为独热编码向量是独立的,并且在相同的维度上没有投影(这意味着某一维度上只有一个 1,其余是 0)。

图 7.2 解释了这个概念:

图 7.2:无投影维度

图 7.2:无投影维度

词嵌入解决了这个问题。有许多技术可以表示词嵌入。但所有这些技术都属于无监督学习算法。其中最著名的方法之一是 Word2Vec 模型,接下来将进行解释。

Word2Vec

Word2Vec 的主要目标是生成词嵌入。它处理语料库,然后为语料库中的每个唯一单词分配一个向量。然而,这个向量不像 one-hot 向量方法那样工作。例如,如果我们的语料库中有 10,000 个单词,我们的 one-hot 编码向量就会有 10,000 个维度,但 Word2Vec 可以进行降维,通常降到几百维。

Word2Vec 的核心思想是一个单词的含义由经常出现在它附近的单词来表示。当一个单词出现在句子中时,它的上下文由它附近的单词集构成。这些单词集位于一个固定大小的窗口内:

图 7.3:*wx* 的上下文词

图 7.3:wx 的上下文词

图 7.3 显示了 wx 的上下文词的示例。

Word2Vec 的概念由 Tomas Mikolov 在 2013 年提出。他提出了一个学习词向量的框架。该方法通过遍历语料库来工作,取出一组包含中心词(在图 7.3 中为 wx)和上下文词(在图 7.3 中,黑色矩形框内显示的词)的一组单词。这些词的向量会不断更新,直到语料库结束。

有两种执行 Word2Vec 的方法:

  • Skip-Gram 模型:在此模型中,输入是放置在中心的单词,之后预测其上下文词。

  • CBOW 模型:该模型的输入是上下文词的向量,输出是中心词。

图 7.4:CBOW 和 Skip-gram 模型表示

图 7.4:CBOW 和 Skip-gram 模型表示

这两种模型都能产生不错的结果,但 Skip-gram 模型在数据量较小的情况下表现较好。我们不会深入讨论这些模型如何生成我们的 Word2Vec,但我们会使用 Gensim 库,本章将介绍它。

Word2Vec 的问题

Word2Vec 在将单词表示为向量空间中的点时有许多优点。它提高了任务的性能,并能够捕捉复杂的单词含义。但它并不完美,存在一些问题:

  • 低效地使用统计数据:它一次捕捉单词的共现。这里的问题是,那些在训练语料库中没有共同出现的单词往往会在平面上变得更接近(这可能会导致歧义),因为没有办法表示它们之间的关系。

  • 需要修改模型的参数,即当语料库的大小发生变化时,模型需要重新训练,而这会消耗大量的时间。

在深入探讨如何用 Word2Vec 解决这些问题之前,我们将介绍 Gensim,这是一个用于创建 Word2Vec 模型的库。

Gensim

Gensim 是一个 Python 库,提供了不同的 NLP 方法。它不同于 NLTK 或 spaCy;那些库侧重于数据的预处理和分析,而 Gensim 提供了处理原始文本(即无结构文本)的方法。

Gensim 的优点如下:

  • Gensim 可以用于超大语料库。它具有内存独立性,这意味着语料库不需要存储在计算机的 RAM 中。同时,它具有内存共享功能来存储训练好的模型。

  • 它可以提供高效的向量空间算法,如 Word2Vec、Doc2Vec、LSI、LSA 等。

  • 它的 API 易于学习。

这些是 Gensim 的缺点:

  • 它没有提供文本预处理的方法,必须与 NLTK 或 spaCy 一起使用,以获得完整的自然语言处理管道。

练习 24:创建词嵌入

在本次练习中,我们将使用一个小语料库和 Gensim 创建我们的词嵌入。一旦模型训练完成,我们将把它打印在一个二维图表上,以检查单词的分布情况。

Gensim 提供了更改一些参数的可能性,以便在我们的数据上进行良好的训练。一些有用的参数如下:

  • Num_features:表示向量的维度(更多维度意味着更高的准确性,但计算成本更高)。在我们的例子中,我们将此参数设置为2(2 维向量)。

  • Window_size:表示固定窗口的大小,用于包含词语的上下文。在我们的例子中,语料库较小,所以此处大小设置为1

  • Min_word_count:最小的词频阈值。

  • Workers:计算机并行运行的线程。在我们的例子中,一个工作线程对于语料库的大小就足够了。

让我们从练习开始:

  1. 导入库。我们将使用 Gensim 模型,Word2Vec:

    import nltk
    import gensim.models.word2vec as w2v
    import sklearn.manifold
    import numpy as np
    import matplotlib.pyplot as plt
    import pandas as pd
    
  2. 定义一个小的随机语料库:

    corpus = ['king is a happy man', 
              'queen is a funny woman',
              'queen is an old woman',
              'king is an old man', 
              'boy is a young man',
              'girl is a young woman',
              'prince is a young king',
              'princess is a young queen',
              'man is happy, 
              'woman is funny,
              'prince is a boy will be king',
              'princess is a girl will be queen']
    
  3. 现在我们将使用spaCy对每个句子进行分词。spaCy的概念在第三章自然语言处理基础中有讲解:

    import spacy
    import en_core_web_sm
    nlp = en_core_web_sm.load()
    def corpus_tokenizer(corpus):
        sentences = []
        for c in corpus:
            doc = nlp(c)
            tokens = []
            for t in doc:
                if t.is_stop == False:
                    tokens.append(t.text)
            sentences.append(tokens)
        return sentences
    sentences = corpus_tokenizer(corpus)
    sentences
    
  4. 现在让我们定义一些变量来创建 Word2Vec 模型:

    num_features=2
    window_size=1
    workers=1
    min_word_count=1
    
  5. 使用 Word2Vec 方法创建模型,种子为 0(这个种子只是用来初始化模型的权重;推荐使用相同的种子以获得相同的结果):

    model = w2v.Word2Vec(size=num_features, window=window_size,workers=workers,min_count=min_word_count,seed=0)
    
  6. 现在,我们将从语料库中构建词汇表。首先,我们需要一个词汇表来训练模型:

    model.build_vocab(sentences)
    
  7. 训练模型。这里的参数是语料库的句子:总词数和训练轮次(在此案例中,1 轮训练就足够):

    model.train(sentences,total_words=model.corpus_count,epochs=1)
    
  8. 现在,我们可以看到当计算两个词语的相似度时,模型是如何工作的:

    model.wv['king']
    model.wv.similarity('boy', 'prince')
    

    图 7.5:计算结果,表示两个词语的相似度

    图 7.5:计算结果,表示两个词语的相似度
  9. 现在,为了打印模型,定义一个包含我们语料库中单词的变量,并定义一个数组来存储每个单词的向量:

    vocab = list(model.wv.vocab)
    X = model.wv[vocab]
    
  10. 使用 pandas 创建一个包含此数据的DataFrame

    df = pd.DataFrame(X, index=vocab, columns=['x', 'y'])
    df
    

    图 7.6:我们的向量坐标

    图 7.6:我们的向量坐标
  11. 创建一个图形,表示每个单词在平面中的位置:

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    for word, pos in df.iterrows():
        ax.annotate(word, pos)
    ax.scatter(df['x'], df['y'])
    plt.show()
    

图 7.7:我们 Word2Vec 模型中项目的位置

图 7.7:我们 Word2Vec 模型中项目的位置

如图 7.7 所示,词语可以在二维空间中表示。如果你有一个较小的语料库,要了解两个词在意义上的相似性,只需要测量这两个词的距离。

现在你知道如何训练自己的 Word2Vec 模型了!

Global Vectors (GloVe)

Global Vectors 是一个词表示模型。它的工作方式和 Word2Vec 模型类似,但增加了一些新特性,使其更加高效。

在开始使用这个模型之前,思考其他创建词向量的方法是很有益的。

语料库中词语出现的统计数据是我们可以用来在无监督算法中利用的第一手信息,因此可以直接捕捉共现计数。为了获得这些信息,我们不需要经过处理的方法;只要有文本数据就足够了。

创建一个共现矩阵 X,并且设置一个固定大小的窗口,我们可以得到词语的新表示。例如,假设这个语料库:

  • 我是 Charles。

  • 我很棒。

  • 我喜欢苹果。

基于窗口的共现矩阵如下所示:

图 7.8: 基于窗口的共现矩阵

图 7.8: 基于窗口的共现矩阵

共现矩阵很容易理解,它统计了一个词在语料库中与另一个词相邻出现的次数。

例如,在第一行,词语"I"的情况下,词语"am"的值为 2,因为"I am"出现了两次。

这种表示方式改进了独热编码,并能捕捉语义和句法信息,但也存在一些问题,比如模型的大小、词汇的稀疏性,并且模型的整体鲁棒性较差。

但在这种情况下,可以通过使用SVD(在第三章自然语言处理基础中解释)来减少矩阵的维度,解决这些问题,公式如下:

  • A = USVT

这样做的结果是好的,词的表示确实有意义,但对于大规模语料库而言,仍然会有问题。

GloVe 方法通过以下方式解决了 Word2Vec 模型的问题:

  • 如果语料库发生变化,训练模型所需的总体时间将减少。

  • 统计数据得到了高效使用。它在处理语料库中不常出现的词语时表现更好。这是 Word2Vec 中的一个问题,即不常见的词语具有相似的向量。

GloVe 结合了前两种方法,以实现快速训练。它能够扩展到庞大的语料库,并且可以用小向量实现更好的性能。

注意

该模型由斯坦福大学创建,是一个开源项目。你可以在github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/tree/master/Lesson07/Exercise25-26/utils找到更多文档。

在下一个练习中,您将学习如何使用 GloVe。

练习 25:使用预训练的 GloVe 查看平面中词语的分布

在本练习中,您将学习如何使用 GloVe 以及如何绘制模型的区域。我们将再次使用 Gensim 库:

注意

要获取模型,您需要从 GitHub 上的utils文件夹下载文件(这是 50 维的模型):

github.com/TrainingByPackt/Artificial-Vision-and-Language-Processing-for-Robotics/tree/master/Chapter%207/utils

  1. 打开您的 Google Colab 界面。

  2. 为本书创建一个文件夹,从 GitHub 下载utils文件夹并将其上传到该文件夹中。

  3. 导入驱动器并按如下方式挂载:

    from google.colab import drive
    drive.mount('/content/drive')
    
  4. 一旦您第一次挂载了您的驱动器,您需要通过点击 Google 提供的 URL 并按下键盘上的Enter键来输入授权码:图 7.9:显示 Google Colab 授权步骤的图片

    图 7.9:显示 Google Colab 授权步骤的图片
  5. 现在您已经挂载了驱动器,您需要设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson07/Exercise25/
    

    注意

    第 5 步中提到的路径可能会根据您在 Google Drive 上的文件夹设置而有所不同。然而,该路径始终以cd /content/drive/My Drive/开头。

    utils文件夹必须出现在您正在设置的路径中。

  6. 导入库:

    from gensim.scripts.glove2word2vec import glove2word2vec
    from gensim.models import KeyedVectors
    import numpy as np
    import pandas as pd
    
  7. 使用 Gensim 提供的glove2word2vec函数创建word2vec模型:

    glove_input_file = 'utils/glove.6B.50d.txt'
    word2vec_output_file = 'utils/glove.6B.50d.txt.word2vec'
    glove2word2vec(glove_input_file, word2vec_output_file)
    

    注意

    在本例中,glove.6B.50d.txt文件已被放置在utils文件夹中。如果您选择将其放在其他地方,路径将相应变化。

  8. 使用glove2word2vec函数生成的文件初始化模型:

    filename = 'utils/glove.6B.50d.txt.word2vec'
    model = KeyedVectors.load_word2vec_format(filename, binary=False)
    
  9. 使用 GloVe,您可以衡量一对词语的相似度。通过计算两个词语之间的相似度并打印出词向量来检查模型是否有效:

    model.similarity('woman', 'queen')
    

    图 7.10:woman 和 queen 的相似度

    图 7.10:woman 和 queen 的相似度
  10. 练习 24创建词嵌入中,我们创建了自己的向量,但在这里,向量已经被创建。要查看一个词的表示向量,我们只需要做以下操作:

    model['woman']
    

    图 7.11:“Woman”向量表示(50 维)

    图 7.11:“Woman”向量表示(50 维)
  11. 我们还可以看到与其他词语最相似的词语。正如在步骤 4 和 5 中所示,GloVe 有许多与词表示相关的功能:

    model.similar_by_word(woman)
    

    图 7.12:与 woman 最相似的词语

    图 7.12:与 woman 最相似的词语
  12. 现在,我们将使用奇异值分解(SVD)来可视化高维数据,以绘制与 woman 最相似的词语。导入必要的库:

    from sklearn.decomposition import TruncatedSVD
    import pandas as pd
    import matplotlib.pyplot as plt
    
  13. 初始化一个 50 维的数组并附加“woman”向量。为了进行这种维度缩减,我们将创建一个矩阵,其行将是每个单词的向量:

    close_words=model.similar_by_word('woman')
    
    arr = np.empty((0,50), dtype='f')
    labels = ['woman']
    #Array with the vectors of the closest words
    arr = np.append(arr, np.array([model['woman']]), axis=0)
    print("Matrix with the word 'woman':\n", arr)
    

    图 7.13:包含“dog”单词的矩阵值

    图 7.13:包含“woman”单词的矩阵值
  14. 现在,我们在矩阵中有了dog这个单词,我们需要附加每个相似单词的向量。将其余的向量添加到矩阵中:

    for w in close_words:
        w_vector = model[w[0]]
        labels.append(w[0])
        arr = np.append(arr, np.array([w_vector]), axis=0)
    arr
    

    这个矩阵大致如下所示:

    图 7.14:包含与“woman”向量最相似的矩阵

    图 7.14:包含与“woman”向量最相似的矩阵
  15. 一旦我们拥有了矩阵中的所有向量,就让我们初始化 TSNE 方法。它是 Sklearn 的一个函数;

    svd = TruncatedSVD(n_components=2, n_iter=7, random_state=42)
    svdvals = svd.fit_transform(arr)
    
  16. 将矩阵转换为二维向量,并使用 pandas 创建一个 DataFrame 来存储它们:

    df = pd.DataFrame(svdvals, index=labels, columns=['x', 'y'])
    df
    

    图 7.15:我们向量在二维中的坐标

    图 7.15:我们向量在二维中的坐标
  17. 创建一个图表来查看平面上的单词:

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    for word, pos in df.iterrows():
        ax.annotate(word, pos)
    ax.scatter(df['x'], df['y'])
    plt.show()
    

图 7.16:与“woman”最相似的单词分布

图 7.16:与“woman”最相似的单词分布

在这里,我们通过降低向量的维度,将其输出为二维图形。在此,我们可以看到单词之间的相似关系。

你已经完成了第 25 号练习!现在你可以选择使用自己的 word2vec 模型或 GloVe 模型。

对话系统

正如我们之前提到的,聊天机器人正变得越来越流行。它们可以帮助人类 24/7,解答问题或进行对话。对话系统能够理解话题,给出合理的回答,并在与人类的对话中检测情感(如积极、消极或中立情感)。这些系统的主要目标是通过模仿人类来进行自然对话。这种像人类一样思考或行为的能力是确保对话中良好用户体验的一个重要因素。洛布纳奖是一个聊天机器人竞赛,参赛的聊天机器人需要接受多种不同句子和问题的测试,最像人类的系统获胜。最受欢迎的对话代理之一是 Mitsuku 聊天机器人(www.pandorabots.com/mitsuku/)。

聊天机器人通常作为文本服务为用户提供信息。例如,在西班牙,最受欢迎的对话代理之一是 Lola,它可以为你提供星座信息(1millionbot.com/chatbot-lola/)。你只需发送一条消息,等待几秒钟即可收到数据。但在 2011 年,苹果公司开发了 Siri,一款能理解语音的虚拟助手,现在我们也有了亚马逊的 Alexa 和谷歌助手。根据系统的输入类型,它们可以分为两类:语音对话系统基于文本的对话系统,这些将在本章后面进行解释。

这不是唯一的对话代理分类方式。根据它们所拥有的知识类型,它们可以分为面向目标和开放领域两类。我们将在本章后面回顾这些分类。

事实上,有许多工具可以在几分钟内创建你自己的聊天机器人。但在本章中,你将学习如何从零开始创建所需的系统知识。

开发聊天机器人的工具

聊天机器人对许多新兴公司帮助巨大。但是,创建聊天机器人时,你是否需要具备深度 NLP 的知识?幸运的是,借助这些工具,即使没有任何 NLP 知识的人,也可以在几个小时内创建一个聊天机器人:

  • Dialogflow: 它可以轻松创建自然语言对话。Dialogflow 是由 Google 拥有的开发工具,提供语音和对话界面。该系统利用 Google 的机器学习技术来找出与用户对话中的适当意图,并且部署在 Google Cloud Platform 上。它支持 14 种以上的语言和多个平台。

  • IBM Watson: Watson Assistant 提供了一个用户友好的界面来创建对话代理。它的工作原理与 Dialogflow 类似,但它部署在 IBM Cloud 上,并且由 IBM Watson 知识库支持。Watson 还提供了多个工具来分析对话生成的数据。

  • LUIS: 语言理解(LUIS)是微软基于机器学习的服务,用于构建自然语言应用程序。这个机器人框架托管在 Azure 云上,并使用微软的知识库。

上述工具是一个复杂的 NLP 系统。在本章中,我们将介绍一种使用预训练的 GloVe 来识别消息意图的基本方法。最新的聊天机器人趋势是语音助手。这些工具使你能够实现一个由语音控制的聊天机器人。有许多方法可以对话代理进行分类。

对话代理的类型

对话代理可以根据输入输出数据类型和知识限制进行多种分组。当一家公司委托创建聊天机器人时,第一步是分析其沟通渠道(文本或语音)以及对话的主题(有限知识或无限制)。

现在,我们将解释许多类型的分组及其各自的特点。

按输入输出数据类型分类

语音控制的虚拟助手不同于基础聊天机器人,我们使用文本进行沟通。根据输入输出类型,我们可以将它们分为两类:

  • 语音对话系统 (SDS): 这些模型旨在通过语音控制,而没有聊天界面或键盘,而是通过麦克风和扬声器来工作。这些系统比普通聊天机器人更难处理,因为它们由不同的模块组成:

图 7.17:SDS 模型结构

图 7.17:SDS 模型结构
  • 图 7.17 显示了 SDS 的模块。由于语音到文本系统需要将人类的语音转换为文本,因此 SDS 的错误概率较高,这可能会失败。一旦语音被转换为文本,对话代理会识别对话的意图并返回响应。在代理返回响应之前,答案会被转换成语音。

  • 基于文本的对话系统:与 SDS(语音对话系统)相比,基于文本的对话系统是基于聊天界面,用户通过键盘和屏幕与聊天机器人进行交互。在本章中,我们将创建一个基于文本的对话聊天机器人。

按系统知识分类

如果聊天机器人能够利用其知识成功地响应每种类型的信息,或者它仅限于一组特定问题,这些对话代理可以按以下方式进行分类:

  • 封闭领域或目标导向(GO):该模型已被训练来识别一组意图。聊天机器人只会理解与这些主题相关的句子。如果对话代理无法识别意图(意图在本章介绍中已解释),它将返回预定义的句子。

  • 开放领域:并非所有聊天机器人都有一组定义的意图。如果系统能够利用 NLG 技术和其他数据源回答各种类型的句子,则被归类为开放领域模型。这些系统的架构比 GO 模型更难构建。

  • 还有第三类对话代理,基于其知识,称为混合领域。它是前述模型的组合,因此,根据句子的不同,聊天机器人将有预定义的响应(与多种响应关联的意图)或没有。

创建基于文本的对话系统

到目前为止,我们已经了解了不同类别的对话代理以及它们如何选择或生成响应。还有许多其他方法可以构建对话代理,NLP 提供了多种不同的方法来实现这一目标。例如,seq2seq(序列到序列)模型能够在给定问题时找到答案。此外,深度语言模型可以基于语料库生成响应,也就是说,如果聊天机器人有一个对话语料库,它可以跟随对话进行。

在本章中,我们将使用斯坦福大学的 GloVe 构建一个聊天机器人。在练习 26中,创建你的第一个对话代理,你将找到我们将要使用的技术的简要介绍,在活动中,我们将创建一个对话代理来控制机器人。

范围定义和意图创建

我们的对话代理将是一个基于文本的对话系统,并且是目标导向的。因此,我们将通过键盘与聊天机器人进行交互,它只会理解与我们创建的意图相关的句子。

在开始创建意图之前,我们需要明确聊天机器人的主要目标是什么(维持一般对话、控制设备、获取信息)以及用户可能提出的不同类型的句子。

一旦我们分析了聊天机器人的可能对话,我们就可以创建意图。每个意图将是一个包含多条训练句子的文本文件。这些训练句子是用户与聊天机器人可能进行的互动。定义这些句子非常重要,因为如果有两个相似的句子具有不同的意图,聊天机器人可能会匹配错误的意图。

注释

对聊天机器人的可能对话进行良好的前期分析将使意图定义变得更加容易。显然,聊天机器人无法理解用户可能说出的所有句子,但它必须能够识别与我们意图相关的句子的含义。

系统还将有一个与意图文件同名的文件,但它将包含与意图相关的响应,而不是训练句子:

图 7.18:系统的文件夹结构

图 7.18:系统的文件夹结构

在图 7.18 中,我们可以看到聊天机器人的结构。意图和响应文件的扩展名是.txt,但也可以将它们保存为.json

GloVe 用于意图检测

在本章的开始,我们回顾了词嵌入、词到向量和全局向量的基础知识。GloVe 使用实数向量表示每个单词,这些向量可以作为各种应用的特征。但在本案例中——构建对话代理——我们将使用完整的句子来训练我们的聊天机器人,而不仅仅是单词。

聊天机器人需要理解一个完整的句子是由一组词语作为向量表示的。将一个序列表示为向量的这种方式称为seq2vec。在内部,对话代理将比较用户的句子与每个意图训练短语,以找到最相似的意思。

此时,已经有了表示序列的向量,这些序列存储在与某个意图相关的文件中。如果使用之前提到的相同过程将所有序列向量合并为一个,那么我们将得到意图的表示。主要思想不仅仅是表示一个句子,而是将整个文档表示为一个向量,这称为Doc2vec。通过这种方法,当用户与聊天机器人互动时,机器人将找到用户短语的意图。

我们系统的最终结构将如下图 7.19 所示:

图 7.19:最终文件夹结构

图 7.19:最终文件夹结构

名为main.py的文件将包含使用位于/data中的 GloVe 模型分析输入句子的不同方法,创建文档向量,以便执行用户句子与意图之间的匹配:

图 7.20:Doc2Vec 转换

图 7.20:Doc2Vec 转换

图 7.20 显示了将一组句子转换为向量的过程,表示一个文档。在此示例中,A.txt 文件是一个包含三句话的意图,每个句子有三个单词,因此每个句子有三个向量。通过合并这些向量,我们获得了每组单词的表示,之后,我们得到了文档向量。

将句子转换为向量的方法可以方便地在文档向量中比较一系列向量。当用户与聊天机器人互动时,用户的短语将被转换为 seq2vec,然后与每个文档向量进行比较,以找到最相似的一个。

练习 26:创建你的第一个对话代理

注意

在你执行练习 25 的同一文件夹中执行练习 26。

在此练习中,你将创建一个聊天机器人来理解基本对话。此练习将涵盖意图和回复的定义、将单词转换为表示文档的向量,并将用户的句子与意图进行匹配。

在开始练习之前,请查看 Google Colab 中的文件夹结构,如图 7.21 所示:

图 7.21:练习 3 的结构

图 7.21:练习 26 的结构

Exercise26.ipynb 文件是我们之前遇到过的 main.py 文件,在 utils 文件夹中,你将找到在上一练习中提到的文件夹结构:

图 7.22:练习 3 的结构 (2)

图 7.22:练习 26 的结构 (2)

文件夹中的回复文件包含聊天机器人在用户互动时可以输出的短语。训练部分定义了每个句子的意图。为了获得每个单词的向量,我们将使用斯坦福的 GloVe,并采用五维:

  1. 首先,我们需要为每个意图定义意图和回复。由于这是一个入门练习,我们将定义三个意图:welcome、how-are-you 和 farewell,并创建一些相关句子(用逗号分隔)。

    “Welcome” 训练句子:Hi friend, Hello, Hi, Welcome.

    “Farewell” 训练句子:Bye, Goodbye, See you, Farewell, Have a good day.

    “How are you” 训练句子:How are you? What is going on? Are you okay?

  2. 一旦我们创建了意图,就需要创建回复。为每个意图文件创建三个具有相同名称的文件,并添加回复。

    “Welcome” 回复:Hello! Hi.

    “How are you?” 回复:I’m good! Very good my friend 😃

    “Farewell” 回复:See you! Goodbye!

  3. 以如下方式导入并挂载驱动:

    from google.colab import drive
    drive.mount('/content/drive')
    
  4. 一旦你首次挂载了驱动,你需要点击 Google 提供的 URL 并按下键盘上的 Enter 键以输入授权码:图 7.23:Google Colab 授权步骤

    图 7.23:Google Colab 授权步骤
  5. 现在你已经挂载了驱动器,接下来需要设置目录的路径:

    /content/drive/My Drive/C13550/Lesson07/Exercise25-26
    
  6. 导入必要的库:

    from gensim.scripts.glove2word2vec import glove2word2vec
    from gensim.models import KeyedVectors
    import numpy as np
    from os import listdir
    
  7. 使用 spaCy,我们将对句子进行分词并删除标点符号。现在,创建一个函数,能够对文档中的每个句子进行分词。在这个练习中,我们将通过将所有词向量组合成一个,创建 Doc2vec。因此,我们将对整个文档进行分词,返回一个包含所有词元的数组。去除停用词是个好习惯,但在本练习中不需要。这个函数的输入是一个句子数组:

    import spacy
    import en_core_web_sm
    nlp = en_core_web_sm.load()
    # return a list of tokens without punctuation marks
    def pre_processing(sentences):
        tokens = []
        for s in sentences:
            doc = nlp(s)
            for t in doc:
                if t.is_punct == False:
                    tokens.append(t.lower_)
        return tokens
    
  8. 加载 GloVe 模型:

    filename = 'utils/glove.6B.50d.txt.word2vec'
    model = KeyedVectors.load_word2vec_format(filename, binary=False)
    
  9. 创建两个列表,分别存储意图文件和响应文件的名称:

    intent_route = 'utils/training/'
    response_route = 'utils/responses/'
    intents = listdir(intent_route)
    responses = listdir(response_route)
    
  10. 创建一个函数,返回一个表示文档的 100 维向量。这个函数的输入是一个文档的词元列表。我们需要初始化一个空的 100 维向量。这个函数的工作是将每个单词的向量相加,然后除以分词文档的长度:

    def doc_vector(tokens):
        feature_vec = np.zeros((50,), dtype="float32")
        for t in tokens:
             feature_vec = np.add(feature_vec, model[t])
        return np.array([np.divide(feature_vec,len(tokens))])
    
  11. 现在,我们准备好读取每个意图文件(位于训练文件夹中),对它们进行分词,并创建一个包含每个文档向量的数组:

    doc_vectors = np.empty((0,50), dtype='f')
    for i in intents:
        with open(intent_route + i) as f:
            sentences = f.readlines()
        sentences = [x.strip() for x in sentences]
        sentences = pre_processing(sentences)
        # adding the document vector to the array doc_vectors
        doc_vectors=np.append(doc_vectors,doc_vector(sentences),axis=0)
    print("Vector representation of each document:\n",doc_vectors)
    

    图 7.24:作为向量表示的文档

    图 7.24:作为向量表示的文档
  12. 使用sklearn中的cosine_similarity函数,创建一个函数,通过比较句子向量与每个文档向量,找到最相似的意图:

    from sklearn.metrics.pairwise import cosine_similarity
    def select_intent(sent_vector, doc_vector):
        index = -1
        similarity = -1 #cosine_similarity is in the range of -1 to 1
        for idx,v in zip(range(len(doc_vector)),doc_vector):
            v = v.reshape(1,-1)
            sent_vector = sent_vector.reshape(1,-1)
            aux = cosine_similarity(sent_vector, v).reshape(1,)
            if aux[0] > similarity:
                index = idx
                similarity = aux
        return index
    
  13. 让我们测试一下我们的聊天机器人。对用户的输入进行分词,并使用最后一个函数(select_intent)来获取相关的意图:

    user_sentence = "How are you"
    user_sentence = pre_processing([user_sentence])
    user_vector = doc_vector(user_sentence).reshape(50,)
    intent = intents[select_intent(user_vector, doc_vectors)]
    intent
    

    图 7.25:预测的文档意图

    图 7.25:预测的文档意图
  14. 创建一个函数来回应用户:

    def send_response(intent_name):
        with open(response_route + intent_name) as f:
            sentences = f.readlines()
        sentences = [x.strip() for x in sentences]
        return sentences[np.random.randint(low=0, high=len(sentences)-1)]
    send_response(intent)
    
  15. 检查测试句子的输出:

    send_response(intent)
    

    输出将如下所示:

    图 7.26:意图 how_are_you 的响应

    图 7.26:意图 how_are_you 的响应
  16. 检查系统是否能处理多条测试句子。

你已经完成了练习 26!你已经准备好构建一个对话代理来控制我们的虚拟机器人。正如你在练习 26 中看到的(第 2 步),你需要对意图进行良好的定义。如果你试图将相同的句子添加到两个不同的意图中,系统可能会失败。

活动 7:创建一个对话代理控制机器人

在这个活动中,我们将创建一个拥有多种意图的聊天机器人。为了完成这个任务,我们将使用斯坦福的 GloVe,正如练习 26中所示,创建你的第一个对话代理。我们将学习如何创建一个程序,等待用户输入句子,当用户与聊天机器人互动时,它将返回一个响应。

场景:你在一家开发安全系统的公司工作。这个安全系统将是一个配备有摄像头的机器人,能够查看环境,并且具有前进或后退的轮子。这个机器人将通过文本控制,你可以输入指令,机器人将执行不同的动作。

  1. 机器人可以执行以下操作:

    向前走。

    向后走。

    旋转:

    向右转 45º。

    向左转 45º。

  2. 识别机器人可以看到的内容。这个活动与练习 26创建你的第一个对话代理中的操作方法相同。为了避免重复编写代码,chatbot_intro.py 文件包含了四个基本方法:

    Pre_processing:对句子进行分词

    Doc_vector:创建文档向量

    Select_intent:找到句子中最相似的意图

    Send_response:发送位于响应文件夹中的句子

    知道了这些方法,核心工作就完成了,所以最重要的是意图的设计。

  3. 我们需要开发四个不同的活动,但旋转活动有两种不同类型。我们将定义五个意图,每个动作一个(旋转有两个)。你可以使用这些句子,但你也可以自由地添加更多的训练句子或动作:

    向后

    向后移动

    向后走

    向后走

    返回

    向后移动

    环境

    你能看到什么?

    环境信息

    拍照

    告诉我你看到的是什么?

    你面前有什么?

    向前

    前进

    向前走

    向前走

    开始移动

    向前

    左侧

    向左转

    向左走

    向左看

    向左转

    左侧

    向右

    向右转

    向右走

    向右看

    向右转

    向右

    你可以在活动/训练文件夹中找到这些文件:

图 7.27:训练句子文件

图 7.27:训练句子文件

注意

本活动的解决方案可在第 323 页找到。

总结

对话代理,也称为聊天机器人,是一种基于文本的对话系统,能够理解人类语言并与人类进行“真实”的对话。为了有效理解人类所说的话,聊天机器人需要将对话分类为不同的意图,即一组表达特定含义的句子。对话代理可以根据输入输出数据的类型和知识限制分为几类。表示意义并不容易。为了为聊天机器人提供有力的支持,需依赖大量的语料库。找到表示一个单词的最佳方式是一个挑战,而独热编码是无效的。独热编码的主要问题是编码向量的大小。如果我们有一个包含 88,000 个单词的语料库,那么编码向量的大小将为 88,000,而且这些单词之间没有任何关系。这里就引入了词嵌入的概念。

词嵌入是将语料库中的单词和句子映射为向量或实数的一系列技术和方法。词嵌入生成每个单词的表示,基于该单词出现的上下文。为了生成词嵌入,我们可以使用 Word2Vec。Word2Vec 处理语料库,并为语料库中的每个唯一单词分配一个向量,并且它可以进行维度减少,通常是几百个维度。

Word2Vec 的核心思想是,一个词的意义由其附近经常出现的词语所决定。当一个词出现在句子中时,它的上下文是由其附近的词语集合形成的。Word2Vec 可以通过两种算法实现:skip-gram 和 CBOW。Word2Vec 的思想是表示词语,这是有用的,但在效率方面存在问题。GloVe 将 Word2Vec 和语料库的统计信息结合起来。GloVe 将这两种方法结合,达到快速训练、可扩展到大规模语料库,并通过小向量实现更好的性能。通过 GloVe,我们能够为聊天机器人提供知识,并结合定义我们意图集的训练句子。

第八章使用 CNN 进行物体识别以引导机器人,将向你介绍如何使用不同的预训练模型进行物体识别。此外,它还将探讨计算机视觉中的最新趋势——通过框架识别图片中每个部分的物体。

第九章:第八章

使用 CNN 进行物体识别以引导机器人

学习目标

本章结束时,你将能够:

  • 解释物体识别是如何工作的

  • 构建一个能够识别物体的网络

  • 构建一个物体识别系统

本章介绍了如何通过构建一个能够基于视频识别物体的网络来实现物体识别。

介绍

物体识别是计算机视觉的一个领域,在这个领域中,机器人能够使用相机或传感器检测环境中的物体,传感器能够提取机器人周围环境的图像。从这些图像中,软件能够检测出每一张图像中的物体,并识别物体的种类。机器能够识别由机器人传感器捕捉的图像或视频中的物体。这使得机器人能够意识到它们的环境。

如果机器人能够识别其环境并利用物体识别获取这些信息,它将能够执行更复杂的任务,例如抓取物体或在环境中移动。在第九章机器人视觉中,我们将看到一个机器人在虚拟环境中执行这些任务。

在这里要执行的任务是检测图像中的特定物体并识别这些物体。这种类型的计算机视觉问题与本书前面讨论的有所不同。为了识别特定物体,我们已经看到,标注这些物体并训练一个卷积神经网络(在第五章计算机视觉中的卷积神经网络中已经介绍过),这种方法效果很好,但如果首先要检测这些物体呢?

之前,我们学习了要识别的物体必须标注上它们所属的相应类别。因此,为了在图像中检测到这些物体,必须在它们周围绘制一个矩形边界框,以便准确定位它们在图像中的位置。神经网络将预测这些物体的边界框及其标签。

用边界框标注物体是一个繁琐且艰难的任务,因此我们不会展示如何为数据集中的图像标注边界框,或如何训练神经网络来识别和检测这些物体。然而,有一个名为labelImg的库,你可以在这个 GitHub 仓库中找到:github.com/tzutalin/labelImg。这个工具可以让你为每个图像中的物体创建边界框。一旦你创建了这些边界框,数据上称之为坐标,你就可以训练一个神经网络来预测图像中每个物体的边界框及相应的标签。

在本章中,我们将使用最先进的 YOLO 网络方法,这些方法已经准备好使用,能够节省你自己编写算法的时间。

多物体识别与检测

多物体识别与检测涉及在一张图像中检测和识别多个物体。这个任务包括用边界框标注每个物体,然后识别该物体的类型。

由于这个原因,市面上有许多预训练的模型可以检测各种物体。名为YOLO的神经网络是最适合此任务的模型之一,并且能够实时工作。YOLO 将在下一章中详细讲解,用于机器人模拟器的开发。

对于本章,YOLO 网络将训练用于识别和检测 80 种不同的类别。这些类别包括:

人、脚踏车、汽车、摩托车、飞机、公交车、火车、卡车、船、交通信号灯、消防栓、停车标志、停车计时器、长椅、鸟、猫、狗、马、羊、牛、大象、熊、斑马、长颈鹿、背包、雨伞、手提包、领带、手提箱、飞盘、滑雪板、单板滑雪、运动球、风筝、棒球棒、棒球手套、滑板、冲浪板、网球拍、瓶子、酒杯、杯子、叉子、刀子、勺子、碗、香蕉、苹果、三明治、橙子、西兰花、胡萝卜、热狗、比萨饼、甜甜圈、蛋糕、椅子、沙发、盆栽、床、餐桌、厕所、电视、笔记本电脑、鼠标、遥控器、键盘、手机、微波炉、烤箱、烤面包机、水槽、冰箱、书、时钟、花瓶、剪刀、泰迪熊、吹风机、牙刷。

在图 8.1 中,您可以看到一个街道场景,YOLO 已检测到其中的行人、汽车和公交车:

图 8.1:YOLO 检测示例

图 8.1:YOLO 检测示例

在本主题中,我们将构建一个针对静态图像的多物体识别与检测系统。

首先,我们将使用一个名为DNN(深度神经网络)的 OpenCV 模块,它只需要几行代码。稍后,我们将使用一个叫做ImageAI的库,它能完成相同的任务,但代码量少于 10 行,并且让你选择具体要检测和识别的物体。

为了在 OpenCV 中实现 YOLO,您需要像本书其他章节一样,使用 OpenCV 导入图像。

练习 24:构建您的第一个多物体检测与识别算法

注意

我们将使用 Google Colab 笔记本进行此任务,因为它不涉及训练算法,而是使用现成的算法。

在这个练习中,我们将使用 YOLO 和 OpenCV 实现一个多物体检测与识别系统。我们将编写一个检测器和识别系统,输入一张图像,检测并识别图像中的物体,然后输出带有这些检测框的图像:

  1. 打开您的 Google Colab 界面。

  2. 导入以下库:

    import cv2
    import numpy as np
    import matplotlib.pyplot as plt
    
  3. 要将图像输入到该网络中,我们需要使用blobFromImage方法:

    注意

    image = cv2.imread('Dataset/obj_det/image6.jpg')
    Width = image.shape[1]
    Height = image.shape[0]
    scale = 0.00392
    

    我们需要加载数据集的类别,对于 YOLO,这些类别存储在Models/yolov3.txt中,你可以在 GitHub 的Chapter 8/Models中找到。我们像这样读取类别:

    # read class names from text file
    classes = None
    with open("Models/yolov3.txt", 'r') as f:
        classes = [line.strip() for line in f.readlines()]
    
  4. 为不同的类别生成不同的颜色:

    COLORS = np.random.uniform(0, 255, size=(len(classes), 3))
    
  5. 读取预训练模型和配置文件:

    net = cv2.dnn.readNet('Models/yolov3.weights', 'Models/yolov3.cfg')
    
  6. 创建输入 blob:

    blob = cv2.dnn.blobFromImage(image.copy(), scale, (416,416), (0,0,0), True, crop=False)
    
  7. 设置网络的输入 blob:

    net.setInput(blob)
    

    为了声明网络,我们使用Models/yolov3.weights(网络的权重)和Models/yolov3.cfg(模型的架构)中的readNet方法:

    注意

    方法、类别、权重和架构文件可以在 GitHub 的Lesson08/Models/文件夹中找到。

    现在我们已经完成了设置,接下来只需要运行并执行代码,这样就能识别并检测图像中的所有物体,下面会详细解释如何操作。

  8. 为了获取网络的输出层,我们声明以下代码中提到的方法,然后运行接口以获得输出层的数组,该数组包含多个检测结果:

    # function to get the output layer names in the architecture
    def get_output_layers(net):
    
        layer_names = net.getLayerNames()
    
        output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]
        return output_layers
    
  9. 创建一个函数,在检测到的物体周围画一个带有类别名称的边界框:

    def draw_bounding_box(img, class_id, confidence, x, y, x_plus_w, y_plus_h):
        label = str(classes[class_id])
        color = COLORS[class_id]
        cv2.rectangle(img, (x,y), (x_plus_w,y_plus_h), color, 2)
        cv2.putText(img, label + " " + str(confidence), (x-10,y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
    
  10. 执行代码:

    # run inference through the network
    # and gather predictions from output layers
    outs = net.forward(get_output_layers(net))
    

    注意

    'outs'是一个预测数组。稍后的操作中,我们将看到需要遍历该数组以获取每个检测结果的边界框、置信度和类别类型。

    物体检测算法常常会对一个物体进行多次检测,这是一个问题。可以通过使用非最大抑制(non-max suppression)来解决这个问题,该方法会删除置信度较低的物体的边界框(即预测为该类别的概率较低),最终只保留置信度最高的边界框。在检测到边界框和置信度,并声明相应的阈值后,可以按如下方式运行该算法:

  11. 这一步是最重要的步骤之一。在这里,我们将收集每个输出层的每次检测的置信度(即每个被检测到的物体)、类别 ID 和边界框,但我们会忽略置信度低于 50%的检测:

    # apply non-max suppression
    class_ids = []
    confidences = []
    boxes = []
    conf_threshold = 0.5
    nms_threshold = 0.4
    indexes = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
    
  12. 对于每个输出层的每次检测,获取置信度、类别 ID 和边界框参数,忽略置信度较低的检测(置信度 < 0.5):

    for out in outs:
        for detection in out:
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            if confidence > 0.5:
                center_x = int(detection[0] * Width)
                center_y = int(detection[1] * Height)
                w = int(detection[2] * Width)
                h = int(detection[3] * Height)
                x = center_x - w / 2
                y = center_y - h / 2
                class_ids.append(class_id)
                confidences.append(float(confidence))
                boxes.append([x, y, w, h])
    
  13. 我们遍历索引列表,使用我们声明的方法打印每个边界框、标签和每个检测的置信度:

    for i in indexes:
        i = i[0]
        box = boxes[i]
        x = box[0]
        y = box[1]
        w = box[2]
        h = box[3]
    
        draw_bounding_box(image, class_ids[i], round(confidences[i],2), round(x), round(y), round(x+w), round(y+h))
    
  14. 最后,我们展示并保存结果图像。OpenCV 也有一个方法可以显示图像,因此不需要使用 Matplotlib:

    # display output image    
    plt.axis("off")
    plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    # save output image to disk
    cv2.imwrite("object-detection6.jpg", image)
    

    输出结果如下:

    图 8.2:YOLO 检测示例

    图 8.2:YOLO 检测示例

    最后,我们需要绘制边界框、类别和置信度。

  15. 现在让我们尝试使用前面提到的步骤做一些其他的示例。你可以在Dataset/obj-det/文件夹中找到图像,输出结果将如图 8.3 所示:

图 8.3:YOLO 检测示例

图 8.3:YOLO 检测示例

ImageAI

还有一种更简单的方法可以实现这一目标。你可以使用ImageAI库,它能够通过几行代码进行物体检测和识别。

该库的 GitHub 仓库链接可以在这里找到:

github.com/OlafenwaMoses/ImageAI

为了安装这个库,你可以通过以下命令使用 pip 进行安装:

pip install https://github.com/OlafenwaMoses/ImageAI/releases/download/2.0.2/imageai-2.0.2-py3-none-any.whl

使用这个库时,我们需要导入一个类:

from imageai.Detection import ObjectDetection

我们导入ObjectDetection类,它将作为神经网络工作。

之后,我们声明将要进行预测的类对象:

detector = ObjectDetection()

我们将要使用的模型必须声明。对于这个库,我们只能使用三种模型:RetinaNet、YOLOV3 和 TinyYOLOV3。YOLOV3 是我们之前使用的相同模型,具有中等的性能和准确性,检测时间也适中。

至于 RetinaNet,它具有更高的性能和准确性,但检测时间较长。

TinyYOLOV3 经过优化,注重速度,具有中等的性能和准确性,但检测时间更快。由于其速度,本模型将在下一个主题中使用。

你只需要修改几行代码,就能让这些模型中的任何一个工作。对于 YOLOV3,需要以下几行代码:

detector.setModelTypeAsYOLOv3()
detector.setModelPath("Models/yolo.h5")
detector.loadModel()

.h5文件包含了 YOLOV3 神经网络的权重和架构。

为了运行推理并获取相应的检测结果,只需一行代码:

detections = detector.detectObjectsFromImage(input_image="Dataset/obj_det/sample.jpg", output_image_path="samplenew.jpg")

这行代码的作用是将一张图像作为输入,检测图像中物体的边界框及其类别。它输出一张标记了这些检测结果的新图像,并列出检测到的物体。

让我们来看一下它如何检测我们在上一个练习中使用的sample.jpg图像:

图 8.4:ImageAI YOLOV3 图像检测

图 8.4:ImageAI YOLOV3 图像检测

ImageAI 还允许你定制要识别的物体。默认情况下,它也能够检测与 YOLO 相同的 80 个类别,这些类别是基于 OpenCV 构建的。

你可以通过传递一个名为CustomObjects的参数来定制只检测你想要的物体,在该参数中,你指定模型要检测哪些物体。此外,检测器的方法也会从detectObjectsFromImage()改为detectCustomObjectsFromImage()。用法如下:

custom_objects = detector.CustomObjects(car=True)
detections = detector.detectCustomObjectsFromImage(custom_objects=custom_objects, input_image="Dataset/obj_det/sample.jpg", output_image_path="samplenew.jpg")

图 8.5:ImageAI YOLOV3 自定义图像检测

图 8.5:ImageAI YOLOV3 自定义图像检测

视频中的多物体识别和检测

静态图像中的多物体识别和检测听起来很棒,那么在视频中检测和识别物体如何呢?

你可以从互联网下载任何视频,尝试检测和识别视频中出现的所有物体。

接下来的处理流程是获取视频的每一帧,并且对于每一帧,检测相应的物体及其标签。

首先声明相应的库:

from imageai.Detection import VideoObjectDetection
from matplotlib import pyplot as plt

imageai库包含一个对象,允许用户对视频进行物体检测与识别:

video_detector = VideoObjectDetection()

我们需要VideoObjectDetection,这样我们才能在视频中检测物体。此外,Matplotlib 也需要用于显示每一帧的检测过程:

图 8.6:ImageAI 单帧物体检测过程

图 8.6:ImageAI 单帧物体检测过程

现在我们首先需要加载模型。你可以根据视频处理的速度需求和所需精度来决定加载哪个模型。YOLOV3 位于 RetinaNet 和 TinyYOLOV3 之间,RetinaNet 精度最高但速度最慢,而 TinyYOLOV3 精度最低但速度最快。我们将使用 YOLOV3 模型,但你也可以自由选择其他两个模型。在声明视频物体检测之后,声明方式与上一个主题相同:

video_detector.setModelTypeAsYOLOv3()
video_detector.setModelPath("Models/yolo.h5")
video_detector.loadModel()

在运行视频检测器之前,我们需要声明一个将在每一帧处理后应用的函数。这个函数不执行检测算法,但它处理每一帧的检测过程。为什么我们要处理每一帧的输出?那是因为我们希望使用 Matplotlib 逐帧展示检测过程。

在声明该方法之前,我们需要声明物体将在其上显示的颜色:

color_index = {'bus': 'red', 'handbag': 'steelblue', 'giraffe': 'orange', 'spoon': 'gray', 'cup': 'yellow', 'chair': 'green', 'elephant': 'pink', 'truck': 'indigo', 'motorcycle': 'azure', 'refrigerator': 'gold', 'keyboard': 'violet', 'cow': 'magenta', 'mouse': 'crimson', 'sports ball': 'raspberry', 'horse': 'maroon', 'cat': 'orchid', 'boat': 'slateblue', 'hot dog': 'navy', 'apple': 'cobalt', 'parking meter': 'aliceblue', 'sandwich': 'skyblue', 'skis': 'deepskyblue', 'microwave': 'peacock', 'knife': 'cadetblue', 'baseball bat': 'cyan', 'oven': 'lightcyan', 'carrot': 'coldgrey', 'scissors': 'seagreen', 'sheep': 'deepgreen', 'toothbrush': 'cobaltgreen', 'fire hydrant': 'limegreen', 'remote': 'forestgreen', 'bicycle': 'olivedrab', 'toilet': 'ivory', 'tv': 'khaki', 'skateboard': 'palegoldenrod', 'train': 'cornsilk', 'zebra': 'wheat', 'tie': 'burlywood', 'orange': 'melon', 'bird': 'bisque', 'dining table': 'chocolate', 'hair drier': 'sandybrown', 'cell phone': 'sienna', 'sink': 'coral', 'bench': 'salmon', 'bottle': 'brown', 'car': 'silver', 'bowl': 'maroon', 'tennis racket': 'palevilotered', 'airplane': 'lavenderblush', 'pizza': 'hotpink', 'umbrella': 'deeppink', 'bear': 'plum', 'fork': 'purple', 'laptop': 'indigo', 'vase': 'mediumpurple', 'baseball glove': 'slateblue', 'traffic light': 'mediumblue', 'bed': 'navy', 'broccoli': 'royalblue', 'backpack': 'slategray', 'snowboard': 'skyblue', 'kite': 'cadetblue', 'teddy bear': 'peacock', 'clock': 'lightcyan', 'wine glass': 'teal', 'frisbee': 'aquamarine', 'donut': 'mincream', 'suitcase': 'seagreen', 'dog': 'springgreen', 'banana': 'emeraldgreen', 'person': 'honeydew', 'surfboard': 'palegreen', 'cake': 'sapgreen', 'book': 'lawngreen', 'potted plant': 'greenyellow', 'toaster': 'ivory', 'stop sign': 'beige', 'couch': 'khaki'}

现在我们将声明应用于每一帧的方法:

def forFrame(frame_number, output_array, output_count, returned_frame):
    plt.clf()
    this_colors = []
    labels = []
    sizes = []
    counter = 0

首先,如图所示,声明函数,并传入帧号、检测数组、每个检测物体的出现次数和帧。我们还声明了相应的变量,用于在每一帧上打印所有检测结果:

    for eachItem in output_count:
        counter += 1
        labels.append(eachItem + " = " + str(output_count[eachItem]))
        sizes.append(output_count[eachItem])
        this_colors.append(color_index[eachItem])

在这个循环中,存储了物体及其相应的出现次数。同时也存储了代表每个物体的颜色:

    plt.subplot(1, 2, 1)
    plt.title("Frame : " + str(frame_number))
    plt.axis("off")
    plt.imshow(returned_frame, interpolation="none")
    plt.subplot(1, 2, 2)
    plt.title("Analysis: " + str(frame_number))
    plt.pie(sizes, labels=labels, colors=this_colors, shadow=True, startangle=140, autopct="%1.1f%%")
    plt.pause(0.01)

在这段代码中,每一帧会打印两个图:一个显示带有相应检测结果的图像,另一个是包含每个检测物体出现次数及其在总次数中占比的图表。

这个输出显示在图 8.6 中。

在最后一个单元格中,为了执行视频检测器,我们写了这几行代码:

plt.show()
video_detector.detectObjectsFromVideo(input_file_path="path_to_video.mp4", output_file_path="output-video" ,  frames_per_second=20, per_frame_function=forFrame,  minimum_percentage_probability=30, return_detected_frame=True, log_progress=True)

第一行初始化 Matplotlib 绘图。

第二行开始视频检测。传递给函数的参数如下:

  • input_file_path:输入视频路径

  • output_file_path:输出视频路径

  • frames_per_second:输出视频的帧率

  • per_frame_function:每处理完一帧进行物体检测后的回调函数

  • minimum_percentage_probability:最低概率值阈值,只有检测到的物体具有最高置信度时才会被考虑

  • return_detected_frame:如果设置为 True,回调函数将接收该帧作为参数。

  • log_progress:如果设置为 True,过程将在控制台中记录。

活动 8:视频中的多物体检测与识别

在此活动中,我们将逐帧处理视频,检测每一帧中的所有可能物体,并将输出视频保存到磁盘:

注意

我们将用于此活动的视频已上传到 GitHub,在Dataset/videos/street.mp4文件夹中:

链接:github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson08/Dataset/videos/street.mp4

  1. 打开 Google Colab 笔记本,挂载磁盘,并导航到第八章所在的位置。

  2. 在笔记本中安装此库,因为它未预安装,可以使用以下命令:

    !pip3 install https://github.com/OlafenwaMoses/ImageAI/releases/download/2.0.2/imageai-2.0.2-py3-none-any.whl
    
  3. 导入本活动开发所需的库并设置matplotlib

  4. 声明你将用于检测和识别物体的模型。

    注意

    你可以在这里找到相关信息:github.com/OlafenwaMoses/ImageAI/blob/master/imageai/Detection/VIDEO.md

    同时注意,所有模型都存储在Models文件夹中。

  5. 声明将在每帧处理后调用的回调方法。

  6. Dataset/videos/文件夹中的street.mp4视频上运行 Matplotlib 和视频检测过程。你也可以尝试在同一目录中的park.mp4视频。

    注意

    本活动的解决方案可在第 326 页找到。

总结

物体识别与检测能够识别图像中的多个物体,围绕这些物体绘制边框并预测它们的类型。

标签框及其标签的标注过程已被解释过,但由于过程庞大,未做深入讲解。相反,我们使用了最先进的模型来识别和检测这些物体。

YOLOV3 是本章使用的主要模型。OpenCV 用于解释如何使用其 DNN 模块运行物体检测管道。ImageAI,作为一种物体检测与识别的替代库,展示了用几行代码写物体检测管道并进行轻松定制的潜力。

最后,通过使用视频,实践了 ImageAI 物体检测管道,将视频中的每一帧传递通过该管道,检测和识别帧中的物体,并使用 Matplotlib 显示它们。

第十章:第九章

机器人视觉

学习目标

本章结束时,您将能够:

  • 使用人工视觉评估物体

  • 将外部框架与 ROS 结合

  • 使用机器人与物体交互

  • 创建一个能够理解自然语言的机器人

  • 开发自己的端到端机器人应用

在本章中,您将学习如何使用 Darknet 和 YOLO。您还将使用 AI 评估物体,并将 YOLO 与 ROS 集成,使您的虚拟机器人能够在虚拟环境中预测物体。

介绍

在前几章中,您接触了许多可能对您来说是新的技术和方法。您学到了许多概念和技术,帮助解决现实世界中的问题。现在,您将运用所有学到的技能,完成本章并构建自己的端到端机器人应用。

在本章中,您将使用一个深度学习框架 Darknet 来构建能够实时识别物体的机器人。该框架将与 ROS 集成,使最终的应用可以应用于任何机器人。此外,重要的是要说明,物体识别可以用于构建不同种类的机器人应用。

您将构建的端到端应用不仅具有学术价值,而且对解决实际问题和应对现实情况非常有用。您甚至可以根据不同情况调整应用的功能。这将为您在与机器人合作时解决实际问题提供很多机会。

Darknet

Darknet 是一个开源神经网络框架,采用 C 和 CUDA 编写。它非常快速,因为支持 GPU 和 CPU 计算。它由计算机科学家 Joseph Redmon 开发,Redmon 专注于人工视觉领域。

尽管我们在本章不会研究所有功能,Darknet 包含了许多有趣的应用。正如我们之前提到的,我们将使用 YOLO,但以下是其他 Darknet 功能的列表:

  • ImageNet 分类:这是一种图像分类器,使用已知的模型,如 AlexNet、ResNet 和 ResNeXt。在使用这些模型对一些 ImageNet 图像进行分类后,会对它们进行比较。比较依据包括时间、准确性、权重等。

  • RNN:循环神经网络用于生成和管理自然语言。它们使用一种叫做 Vanilla RNN 的架构,包含三个循环模块,能够在语音识别和自然语言处理等任务中取得良好的效果。

  • Tiny Darknet:由另一个图像分类器组成,但这次生成的模型要轻得多。该网络获得与 Darknet 类似的结果,但模型大小仅为 4 MB。

    注意

    除了前面提到的,Darknet 还有其他一些应用。您可以通过访问其网站 pjreddie.com/darknet/ 获取更多关于该框架的信息。

Darknet 的基本安装

Darknet 的基础安装不会让你使用 YOLO 的全部功能,但足以检查其工作原理并进行第一次对象检测预测。它不会让你使用 GPU 进行实时预测。对于更复杂的任务,请参阅下一部分。

注意

有关 Darknet 的基础和高级安装的详细步骤,请参阅前言,第 vii 页。

YOLO

YOLO 是一个基于深度学习的实时对象检测系统,包含在 Darknet 框架中。其名称来源于缩写 You Only Look Once,指的是 YOLO 的处理速度之快。

在网站上(pjreddie.com/darknet/yolo/),作者添加了一张图像,在其中将该系统与其他具有相同目的的系统进行了比较:

图 9.1:对象检测系统的比较

图 9.1:对象检测系统的比较

在前面的图表中,y 轴表示 mAP(平均精度),x 轴表示时间(毫秒)。因此,你可以看到,YOLO 在更短时间内达到了比其他系统更高的 mAP。

同样,理解 YOLO 的工作原理也很重要。它使用一个神经网络,应用于整张图像,并将图像分割成不同的部分,预测边界框。这些边界框类似于矩形,用来标示出某些物体,后续过程将对其进行识别。YOLO 之所以快速,是因为它只需要对神经网络进行一次评估就能进行预测,而其他识别系统需要多次评估。

上述网络具有 53 层卷积层,交替使用 3x3 和 1x1 的层。以下是从 YOLO 作者论文中提取的架构图(pjreddie.com/media/files/papers/YOLOv3.pdf):

图 9.2:YOLO 架构

图 9.2:YOLO 架构

使用 YOLO 进行图像分类的第一步

在本节中,我们将进行第一次 YOLO 预测。你需要完成基本安装。让我们开始识别单张图像中的物体:

  1. 我们将使用一个预训练模型以避免训练过程,所以第一步是下载 Darknet 目录中的网络权重:

    cd <darknet_path>
    wget https://pjreddie.com/media/files/yolov3.weights
    
  2. 之后,我们将使用 YOLO 进行预测。在这个第一个示例中,我们尝试识别一个单一物体,一只狗。这是我们使用的样本图像:

图 9.3:要预测的样本图像

图 9.3:要预测的样本图像

将此图像保存为 .jpg 文件,并在 Darknet 目录中运行 YOLO:

./darknet detect cfg/yolov3.cfg yolov3.weights dog.jpg

当执行完成后,你应该会看到类似以下的输出:

图 9.4:预测输出

图 9.4:预测输出

如你所见,YOLO 以 100% 准确度检测到图像中的狗。它还生成了一个名为 predictions.jpg 的新文件,在该文件中可以看到狗在图像中的位置。你可以从 Darknet 目录中打开它:

图 9.5:图像中的识别物体

图 9.5:图像中的识别物体

使用 YOLO 的另一种可能性是通过一次执行对多张图像进行预测。为此,你需要输入与之前相同的命令,但这次不要输入图像路径:

./darknet detect cfg/yolov3.cfg yolov3.weights

在这种情况下,你将看到以下输出:

图 9.6:预测命令输出

如你所见,它要求你输入一张图像。例如,你可以通过输入 dog.jpg 来使用之前的图像。然后,它会要求你输入另一张图像的路径。这样,你可以对所有想要预测的图像进行预测。这可能是一个示例:

图 9.7:图像预测后的输出

图 9.7:图像预测后的输出

如果你这样做,你将得到这张图像:

图 9.8:图像预测

图 9.8:图像预测

在使用 YOLO 时,还有一个有趣的命令需要了解。它可以用来修改检测阈值。

注意

检测阈值是判断预测是否正确的准确度限制。例如,如果你将阈值设置为 0.75,那么准确度低于此值的物体将不会被视为正确预测。

默认情况下,YOLO 会将预测准确度为 0.25 或更高的物体包括在输出中。你可以使用以下命令的最后一个标志来更改阈值:

./darknet detect cfg/yolov3.cfg yolov3.weights dog2.jpg -thresh 0.5

如你所料,前面的命令将阈值设置为 0.5。我们来看看一个实际的示例。按照以下步骤来测试阈值修改的功能:

  1. 对图像进行预测,直到你找到一个预测准确度低于 100% 的图像。我们将使用这个示例,在该示例中,狗的识别准确度为 60%:图 9.9:准确度低于 100% 的示例图像

    图 9.9:准确率低于 100% 的示例图像
  2. 现在,使用 predict 命令修改检测阈值。由于狗的检测准确度为 60%,如果我们将阈值改为 70%,则不应检测到任何物体:

    ./darknet detect cfg/yolov3.cfg yolov3.weights dog2.jpg -thresh 0.7
    
  3. 如果我们检查 predictions 文件,就可以确认狗没有被检测到。因此,你可以看到阈值在识别中的重要作用:

图 9.10:修改阈值后的最终预测

图 9.10:修改阈值后的最终预测

YOLO 在网络摄像头上

一旦你用 YOLO 做出了第一次预测,接下来是尝试这个系统的一个更有趣的功能。你将通过将 YOLO 连接到个人网络摄像头来检测你自己的真实物体。为此,你必须完成高级安装,因为它需要 GPU 和 OpenCV:

  1. 确保你的网络摄像头已连接并且能被系统检测到。

  2. 在 Darknet 目录下输入以下命令:

    ./darknet detector demo cfg/coco.data cfg/yolov3.cfg yolov3.weights
    
  3. 尝试识别环境中的物体;例如,我们已在书架上检测到书籍:

图 9.11:使用网络摄像头识别的书籍

图 9.11:使用网络摄像头识别的书籍

练习 28:使用 YOLO 编程

在本练习中,我们将学习如何使用 YOLO 和 Python 进行预测。我们将创建一个数据集,并检查数据集中包含某个特定物体的图像数量。要构建数据集,请查看以下图像:

图 9.12:数据集中包含的图像

图 9.12:数据集中包含的图像

如你所见,这是一个非常简单的数据集,包含了动物和景观图像。你将要实现的 Python 程序需要获取包含狗的图像数量。

我们将从 GitHub 克隆 Darknet 文件开始:

git clone https://github.com/pjreddie/darknet
cd darknet
make
  1. 在 Darknet 目录下创建一个名为 dataset 的新文件夹。

  2. 将这些图像或你选择的其他图像放入新文件夹中。

    注意

    图像可以在 GitHub 的 Chapter 9/exercise28/dataset/ 文件夹中找到

    URL: github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/tree/master/Lesson09/Exercise28/dataset

  3. 创建一个 Python 文件 excercise1.py,并开始实现。

    导入 Python 及所需的库:

    #!usr/bin/env python
    import sys, os
    
  4. 告诉系统在哪里可以找到 Darknet 框架,然后导入它。如果你已经在 Darknet 目录下创建了文件,可以按以下方式操作:

    注意

    sys.path.append(os.path.join(os.getcwd(),'python/'))
    import darknet as dn
    
  5. 告诉 Darknet 使用哪个 GPU 执行程序:

    注意

    dn.set_gpu(0)
    
  6. 配置你将用来进行预测的网络。在这种情况下,我们使用的是与之前相同的配置:

    net = dn.load_net("cfg/yolov3.cfg", "yolov3.weights", 0)
    meta = dn.load_meta("cfg/coco.data")
    

    注意

    注意输入的路径;如果你的 Python 文件不在 Darknet 文件夹内,路径可能会发生变化。

  7. 声明变量以计算图像的总数和包含狗的图像数量:

    dog_images = 0
    number_of_images = 0
    
  8. 实现一个循环,遍历数据集中的文件:

    for file in os.listdir("dataset/"):
    
  9. 使用 Darknet 的 detect 方法识别每张图像中的物体:

        filename = "dataset/" + file
        r = dn.detect(net, meta, filename)
    
  10. 遍历已识别的物体,检查它们中是否有狗。如果有,将狗图像计数器加一,并停止检查其余物体。总计数器也加一:

    注意

        for obj in r:
            if obj[0] == "dog":
                dog_images += 1
                break
        number_of_images += 1
    
  11. 最后,打印得到的结果。例如:

    print("There are " + str(dog_images) + "/" + str(number_of_images) + " images containing dogs")
    

    注意

    cd ..
     wget https://pjreddie.com/media/files/yolov3.weights
    python exercise28.py
    

    注意

    这里的 cd .. 命令切换到文件所在的目录,并下载权重文件和运行脚本。

    例如,cd <your_script_location>

你可以通过运行脚本来测试它是否按预期工作。如果你使用了建议的数据集,输出应该如下所示:

图 9.13:练习 1 最终输出

图 9.13:练习 28 最终输出

ROS 集成

现在,你已经学会了如何在常见的 Python 程序中使用 YOLO。接下来,看看如何将其与机器人操作系统(ROS)集成,以便你可以在实际的机器人问题中使用它。你可以将它与任何机器人相机结合,允许机器人检测和识别物体,实现人工视觉的目标。在完成以下练习后,你将能够独立完成它。

练习 29:ROS 与 YOLO 集成

本练习包括一个新的 ROS 节点实现,该节点使用 YOLO 识别物体。我们将使用 TurtleBot 进行测试,这是我们在第六章 机器人操作系统(ROS)中使用的 ROS 模拟器,但它将很容易适配任何带有相机的机器人。以下是必须遵循的步骤:

  1. 在你的 catkin 工作空间中创建一个新的包来包含集成节点。使用以下命令来包含正确的依赖项:

    cd ~/catkin_ws/
    source devel/setup.bash
    roscore
    cd src
    catkin_create_pkg exercise29 rospy cv_bridge geometry_msgs image_transport sensor_msgs std_msgs
    
  2. 切换到包文件夹并创建一个新的scripts目录。然后,创建 Python 文件并使其可执行:

    cd exercise29
    mkdir scripts
    cd scripts
    touch exercise29.py
    chmod +x exercise29.py
    
  3. 从实现开始。

    导入你将用于节点实现的库。你需要sysos来从路径中导入 Darknet,OpenCV来处理图像,以及从sensor_msgs导入Image来发布它们:

    import sys
    import os
    from cv_bridge import CvBridge, CvBridgeError
    from sensor_msgs.msg import Image
    

    告诉系统在哪里找到 Darknet:

    sys.path.append(os.path.join(os.getcwd(), '/home/alvaro/Escritorio/tfg/darknet/python/'))
    

    注意

    import darknet as dn
    

    创建一个类,在其中编写节点逻辑和其构造函数:

    class Exercise29():
        def __init__(self):
    

    编写构造函数:

    现在,我们将初始化节点:

            rospy.init_node('Exercise29', anonymous=True)
    

    创建一个桥接对象:

            self.bridge = CvBridge()
    

    订阅相机话题:

            self.image_sub = rospy.Subscriber("camera/rgb/image_raw", Image, self.imageCallback)
    

    创建一个变量来存储获取到的图像:

            self.imageToProcess = None
    

    为 YOLO 配置定义相应的路径:

            cfgPath =  "/home/alvaro/Escritorio/tfg/darknet/cfg/yolov3.cfg"
            weightsPath = "/home/alvaro/Escritorio/tfg/darknet/yolov3.weights"
            dataPath = "/home/alvaro/Escritorio/tfg/darknet/cfg/coco2.data"
    

    注意

            self.net = dn.load_net(cfgPath, weightsPath, 0)
            self.meta = dn.load_meta(dataPath)
    

    定义用于存储图像的名称:

            self.fileName = 'predict.jpg'
    

    实现回调函数以获取 OpenCV 格式的图像:

        def imageCallback(self, data):
            self.imageToProcess = self.bridge.imgmsg_to_cv2(data, "bgr8")
    

    创建一个函数,用于对获取的图像进行预测。节点必须不断进行预测,直到用户停止执行。这将通过将图像存储到磁盘并使用检测函数进行预测来完成。最后,结果将持续打印:

        def run(self):
            while not rospy.core.is_shutdown():
                if(self.imageToProcess is not None):
                    cv2.imwrite(self.fileName, self.imageToProcess)
                    r = dn.detect(self.net, self.meta, self.fileName)
                    print r
    

    实现主程序入口。在这里,你需要初始化 Darknet,创建已创建类的实例,并调用其主方法:

    if __name__ == '__main__':
        dn.set_gpu(0)
        node = Exercise29()
        try:
            node.run()
        except rospy.ROSInterruptException:
            pass
    
  4. 测试节点是否按预期工作。

    打开终端并启动 ROS:

    cd ../../
    cd ..
    source devel/setup.bash
    roscore
    

    打开另一个终端并运行 Gazebo 与 TurtleBot:

    cd ~/catkin_ws
    source devel/setup.bash
    roslaunch turtlebot_gazebo turtlebot_world.launch
    

    插入 YOLO 可识别的物体并让 TurtleBot查看它们。你可以通过点击位于左上角的插入按钮插入新物体。例如,你可以插入一个碗:

    图 9.14:在 Gazebo 中插入的碗

    图 9.14:在 Gazebo 中插入的碗
  5. 打开一个新的终端并运行创建的节点:

    cd ~/catkin_ws
    source devel/setup.bash
    rosrun exercise29 exercise29.py
    

    如果你使用了一个碗,检查你是否得到了如下输出:

图 9.15:节点预测的物体

图 9.15:节点预测的物体

活动 9:机器人安全守卫

假设一个场景,类似于第六章,第 6 活动,模拟器与传感器活动:你在一家机器人公司工作,该公司最近获得了一个新客户——一家购物中心。客户希望你的公司为购物中心提供一些机器人,晚上防止盗窃。这些机器人必须将任何人视为小偷,如果检测到小偷,则需要警告客户。

使用 Gazebo 为 TurtleBot 或其他模拟器提供所需的功能。你应该按照以下步骤进行:

  1. 为存储所需节点创建一个 catkin 包。

  2. 现在,实现第一个节点。它应该从机器人摄像头获取图像,并对其运行 YOLO。

  3. 接下来,它应该以字符串格式发布检测到的物体列表。

  4. 实现第二个节点。它应该订阅发布检测到的物体的主题,并获取它们。最后,它应该检查这些物体中是否有一个人,并在是的话打印警告信息。

  5. 同时运行两个节点。

    注意

    虽然这不是本活动的主要目标,但将这些节点的执行与另一个节点(例如用于移动机器人的节点,可以使用第六章,机器人操作系统(ROS)中实现的节点)结合起来会很有趣。

    本活动的解决方案可以在第 330 页找到。

总结

我们现在已经达到了本书的目标,并为机器人构建了一个端到端的应用程序。这只是一个示例应用程序;然而,你可以使用在本书中学到的技术,构建其他机器人应用程序。在本章中,你还学习了如何安装和使用 Darknet 和 YOLO。你学习了使用 AI 评估物体,并集成 YOLO 和 ROS,使你的虚拟机器人能够预测物体。

你已经学习了如何使用自然语言处理命令控制机器人,并研究了本书中的各种模型,如 Word2Vec、GloVe 嵌入技术和非数值数据。在此之后,你还与 ROS 一起工作,并构建了一个对话代理来管理你的虚拟机器人。你开发了构建一个功能性应用所需的技能,能够与 ROS 集成,从你的环境中提取有用的信息。你还使用了不仅对机器人有用的工具;你也可以使用人工视觉和语言处理。

本书的结尾,我们鼓励你开始自己的机器人项目,并且在本书中练习你最喜欢的技术。现在你可以比较不同的工作方法,并探索计算机视觉、算法和极限。始终记住,机器人是一种机器,它可以拥有你希望它具备的行为。

第十一章:附录

关于

本节内容旨在帮助学生完成书中的活动。它包括学生为实现活动目标而需要执行的详细步骤。

第一章:机器人学基础

活动 1:使用 Python 进行机器人定位与里程计

解决方案

from math import pi
def wheel_distance(diameter, encoder, encoder_time, wheel, movement_time):
    time = movement_time / encoder_time
    wheel_encoder = wheel * time
    wheel_distance = (wheel_encoder * diameter * pi) / encoder

    return wheel_distance
from math import cos,sin
def final_position(initial_pos,wheel_axis,angle):
    final_x=initial_pos[0]+(wheel_axis*cos(angle))
    final_y=initial_pos[1]+(wheel_axis*sin(angle))
    final_angle=initial_pos[2]+angle

    return(final_x,final_y,final_angle)
def position(diameter,base,encoder,encoder_time,left,right,initial_pos,movement_time):
#First step: Wheels completed distance
    left_wheel=wheel_distance(diameter,encoder,encoder_time,left,movement_time)
    right_wheel=wheel_distance(diameter,encoder,encoder_time,right,movement_time)
#Second step: Wheel's central axis completed distance
    wheel_axis=(left_wheel+right_wheel)/2
#Third step: Robot's rotation angle
    angle=(right_wheel-left_wheel)/base
#Final step: Final position calculus
    final_pos=final_position(initial_pos,wheel_axis,angle)

    returnfinal_pos
position(10,80,76,5,600,900,(0,0,0),5)

注意:

若要进行进一步观察,您可以将车轮的直径更改为 15 cm 并检查输出的差异。类似地,您可以更改其他输入值并检查输出的差异。

第二章:计算机视觉简介

活动 2:从 Fashion-MNIST 数据集中分类 10 种衣物

解决方案

  1. 打开你的 Google Colab 界面。

  2. 创建一个书籍文件夹,从 GitHub 下载 Dataset 文件夹,并将其上传到该文件夹中。

  3. 按如下方式导入驱动并挂载:

    from google.colab import drive
    drive.mount('/content/drive')
    

    一旦你第一次挂载了你的驱动器,你将需要输入 Google 提供的授权代码,点击给定的 URL 并按下键盘上的 Enter 键:

    图 2.39:展示 Google Colab 授权步骤的图像

    图 2.38:展示 Google Colab 授权步骤的图像
  4. 现在你已经挂载了驱动器,你需要设置目录的路径:

    cd /content/drive/My Drive/C13550/Lesson02/Activity02/
    
  5. 加载数据集并展示五个样本:

    from keras.datasets import fashion_mnist
    (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
    

    输出如下:

    图 2.40:加载带有五个样本的数据集

    图 2.39:加载带有五个样本的数据集
    import random
    from sklearn import metrics
    from sklearn.utils import shuffle
    random.seed(42)
    from matplotlib import pyplot as plt
    for idx in range(5):
        rnd_index = random.randint(0, 59999)
        plt.subplot(1,5,idx+1),plt.imshow(x_train[idx],'gray')
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    图 2.40:来自 Fashion-MNIST 数据集的图像样本
  6. 预处理数据:

    import numpy as np
    from keras import utils as np_utils
    x_train = (x_train.astype(np.float32))/255.0
    x_test = (x_test.astype(np.float32))/255.0
    x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
    x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)
    y_train = np_utils.to_categorical(y_train, 10)
    y_test = np_utils.to_categorical(y_test, 10)
    input_shape = x_train.shape[1:]
    
  7. 使用 Dense 层构建神经网络架构:

    from keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau
    from keras.layers import Input, Dense, Dropout, Flatten
    from keras.preprocessing.image import ImageDataGenerator
    from keras.layers import Conv2D, MaxPooling2D, Activation, BatchNormalization
    from keras.models import Sequential, Model
    from keras.optimizers import Adam, Adadelta
    def DenseNN(inputh_shape):
        model = Sequential()
        model.add(Dense(128, input_shape=input_shape))
        model.add(BatchNormalization())
        model.add(Activation('relu'))
        model.add(Dropout(0.2))
        model.add(Dense(128))
        model.add(BatchNormalization())
        model.add(Activation('relu'))
        model.add(Dropout(0.2))
        model.add(Dense(64))
        model.add(BatchNormalization())
        model.add(Activation('relu'))
        model.add(Dropout(0.2))
        model.add(Flatten())
        model.add(Dense(64))
        model.add(BatchNormalization())
        model.add(Activation('relu'))
        model.add(Dropout(0.2))
        model.add(Dense(10, activation="softmax"))
        return model
    model = DenseNN(input_shape)
    

    注意:

    本活动的完整代码文件可以在 GitHub 的 Lesson02 | Activity02 文件夹中找到。

  8. 编译并训练模型:

    optimizer = Adadelta()
    model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
    ckpt = ModelCheckpoint('model.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False)
    model.fit(x_train, y_train, batch_size=128, epochs=20, verbose=1, validation_data=(x_test, y_test), callbacks=[ckpt])
    

    获得的准确率为 88.72%。这个问题更难解决,所以我们达成的准确率低于上一个练习。

  9. 做出预测:

    import cv2
    images = ['ankle-boot.jpg', 'bag.jpg', 'trousers.jpg', 't-shirt.jpg']
    for number in range(len(images)):
        imgLoaded = cv2.imread('Dataset/testing/%s'%(images[number]),0)
        img = cv2.resize(imgLoaded, (28, 28))
        img = np.invert(img)
    cv2.imwrite('test.jpg',img)
        img = (img.astype(np.float32))/255.0
        img = img.reshape(1, 28, 28, 1)
        plt.subplot(1,5,number+1),plt.imshow(imgLoaded,'gray')
        plt.title(np.argmax(model.predict(img)[0]))
        plt.xticks([]),plt.yticks([])
    plt.show()
    

    输出将如下所示:

图 2.42:使用神经网络进行衣物预测

图 2.41:使用神经网络进行衣物预测

它已经正确地分类了包和 T 恤,但它未能正确分类靴子和裤子。这些样本与它所训练的样本差异很大。

第三章:自然语言处理基础

活动 3:处理语料库

解决方案

  1. 导入 sklearnTfidfVectorizerTruncatedSVD 方法:

    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.decomposition import TruncatedSVD
    
  2. 加载语料库:

    docs = []
    ndocs = ["doc1", "doc2", "doc3"]
    for n in ndocs:
        aux = open("dataset/"+ n +".txt", "r", encoding="utf8")
        docs.append(aux.read())
    
  3. 使用 spaCy,让我们添加一些新的停用词,标记化语料库,并删除这些停用词。去除这些词后的新语料库将存储在一个新变量中:

    import spacy
    import en_core_web_sm
    from spacy.lang.en.stop_words import STOP_WORDS
    nlp = en_core_web_sm.load()
    nlp.vocab["\n\n"].is_stop = True
    nlp.vocab["\n"].is_stop = True
    nlp.vocab["the"].is_stop = True
    nlp.vocab["The"].is_stop = True
    newD = []
    for d, i in zip(docs, range(len(docs))):
        doc = nlp(d)
        tokens = [token.text for token in doc if not token.is_stop and not token.is_punct]
        newD.append(' '.join(tokens))
    
  4. 创建 TF-IDF 矩阵。我将添加一些参数来改善结果:

    vectorizer = TfidfVectorizer(use_idf=True, 
                                ngram_range=(1,2), 
                                smooth_idf=True,
                                max_df=0.5)
    X = vectorizer.fit_transform(newD)
    
  5. 执行 LSA 算法:

    lsa = TruncatedSVD(n_components=100,algorithm='randomized',n_iter=10,random_state=0)
    lsa.fit_transform(X)
    
  6. 使用 pandas,我们看到一个排序过的 DataFrame,其中包含每个概念的术语权重和每个特征的名称:

    import pandas as pd
    import numpy as np
    dic1 = {"Terms": terms, "Components": lsa.components_[0]}
    dic2 = {"Terms": terms, "Components": lsa.components_[1]}
    dic3 = {"Terms": terms, "Components": lsa.components_[2]}
    f1 = pd.DataFrame(dic1)
    f2 = pd.DataFrame(dic2)
    f3 = pd.DataFrame(dic3)
    f1.sort_values(by=['Components'], ascending=False)
    f2.sort_values(by=['Components'], ascending=False)
    f3.sort_values(by=['Components'], ascending=False)
    

    输出如下:

图 3.25:最相关词语的输出示例 (f1)

图 3.26:概念中最相关单词的输出示例(f1)

注意:

不用担心关键字与您的不完全相同,只要这些关键字代表一个概念,那就是有效结果。

第四章:自然语言处理中的神经网络

活动 4:预测序列中的下一个字符

解决方案

  1. 导入解决此任务所需的库:

    import tensorflow as tf
    from keras.models import Sequential
    from keras.layers import LSTM, Dense, Activation, LeakyReLU
    import numpy as np
    
  2. 定义字符序列,并将其乘以 100:

    char_seq = 'qwertyuiopasdfghjklñzxcvbnm' * 100
    char_seq = list(char_seq)
    
  3. 创建一个 char2id 字典,将每个字符与一个整数关联:

    char2id = dict([(char, idx) for idx, char in enumerate(set(char_seq))])
    
  4. 将字符的句子划分为时间序列。时间序列的最大长度为五,因此我们将得到五个字符的向量。同时,我们将创建即将到来的向量。y_labels 变量表示我们词汇表的大小。我们稍后会使用这个变量:

    maxlen = 5
    sequences = []
    next_char = []
    
    for i in range(0,len(char_seq)-maxlen):
        sequences.append(char_seq[i:i+maxlen])
        next_char.append(char_seq[i+maxlen])
    
    y_labels = len(char2id)
    print("5 first sequences: {}".format(sequences[:5]))
    print("5 first next characters: {}".format(next_char[:5]))
    print("Total sequences: {}".format(len(sequences)))
    print("Total output labels: {}".format(y_labels))
    
  5. 到目前为止,我们有一个序列变量,它是一个数组的数组,包含字符的时间序列。char 是一个数组,包含即将出现的字符。现在,我们需要对这些向量进行编码,因此我们来定义一个方法,用 char2id 信息对字符数组进行编码:

    def one_hot_encoder(seq, ids):
        encoded_seq = np.zeros([len(seq),len(ids)])
        for i,s in enumerate(seq):
            encoded_seq[i][ids[s]] = 1
        return encoded_seq
    
  6. 将变量编码为 one-hot 向量。其形状为 x = (2695,5,27) 和 y = (2695,27):

    x = np.array([one_hot_encoder(item, char2id) for item in sequences])
    y = np.array(one_hot_encoder(next_char, char2id))
    x = x.astype(np.int32)
    y = y.astype(np.int32)
    
    print("Shape of x: {}".format(x.shape))
    print("Shape of y: {}".format(y.shape))
    

    图 4.35:变量编码为 OneHotVectors

    图 4.35:变量编码为 OneHotVectors
  7. 将数据分为训练集和测试集。为此,我们将使用 sklearn 的 train_test_split 方法:

    from sklearn.model_selection import train_test_split
    
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, shuffle=False)
    print('x_train shape: {}'.format(x_train.shape)) 
    print('y_train shape: {}'.format(y_train.shape))  
    print('x_test shape: {}'.format(x_test.shape)) 
    print('y_test shape: {}'.format(y_test.shape))
    

    图 4.36:将数据分为训练集和测试集

    图 4.36:将数据分为训练集和测试集
  8. 数据准备好插入到神经网络中后,创建一个包含两层的 Sequential 模型:

    第一层:LSTM,包含八个神经元(激活函数为 tanh)。input_shape 是序列的最大长度和词汇表的大小。因此,基于我们数据的形状,我们不需要进行任何形状重塑。

    第二层:Dense,包含 27 个神经元。这就是我们成功完成任务的方式。使用 LeakyRelu 激活函数将给您一个良好的分数。但为什么?我们的输出中有许多零值,因此网络可能会失败并仅返回一个零向量。使用 LeakyRelu 可以避免这个问题:

    model = Sequential()
    model.add(LSTM(8,input_shape=(maxlen,y_labels)))
    model.add(Dense(y_labels))
    model.add(LeakyReLU(alpha=.01)) 
    
    model.compile(loss='mse', optimizer='rmsprop')
    
  9. 训练模型。我们使用的 batch_size 是 32,训练了 25 个周期:

    history = model.fit(x_train, y_train, batch_size=32, epochs=25, verbose=1)
    

    图 4.37:使用批次大小 32 和 25 个周期进行训练

    图 4.37:使用批次大小 32 和 25 个周期进行训练
  10. 计算您的模型的误差。

    print('MSE: {:.5f}'.format(model.evaluate(x_test, y_test)))
    

    图 4.38:模型中显示的错误

    图 4.38:模型中显示的错误
  11. 预测测试数据,并查看命中率的平均百分比。使用该模型,您将获得超过 90% 的平均准确率:

    prediction = model.predict(x_test)
    
    errors = 0
    for pr, res in zip(prediction, y_test):
        if not np.array_equal(np.around(pr),res):
            errors+=1
    
    print("Errors: {}".format(errors))
    print("Hits: {}".format(len(prediction) - errors))
    print("Hit average: {}".format((len(prediction) - errors)/len(prediction)))
    

    图 4.39:预测测试数据

    图 4.39:预测测试数据
  12. 为了完成这个任务,我们需要创建一个函数,该函数接受一个字符序列并返回下一个预测值。为了对模型的预测结果进行解码,我们首先编写一个解码方法。这个方法只是寻找预测中的最大值,并在 char2id 字典中获取相应的字符键。

    def decode(vec):
        val = np.argmax(vec)
        return list(char2id.keys())[list(char2id.values()).index(val)]
    
  13. 创建一个方法来预测给定句子中的下一个字符:

    def pred_seq(seq):
        seq = list(seq)
        x = one_hot_encoder(seq,char2id)
        x = np.expand_dims(x, axis=0)
        prediction = model.predict(x, verbose=0)
        return decode(list(prediction[0]))
    
  14. 最后,引入序列‘tyuio’来预测即将到来的字符。它将返回‘p’:

    pred_seq('tyuio')
    

图 4.40:带有预测序列的最终输出

图 4.40:带有预测序列的最终输出

恭喜!你已经完成了活动。你可以预测一个值并输出一个时间序列。这在金融中也非常重要,即预测未来的价格或股票的变动。

你可以更改数据并预测你想要的内容。如果添加语言语料库,你将从自己的 RNN 语言模型中生成文本。因此,我们未来的对话代理可以生成诗歌或新闻文本。

第五章:计算机视觉的卷积神经网络

活动 5:利用数据增强正确分类花卉图像

解决方案

  1. 打开你的 Google Colab 界面。

    注意:

    import numpyasnp
    classes=['daisy','dandelion','rose','sunflower','tulip']
    X=np.load("Dataset/flowers/%s_x.npy"%(classes[0]))
    y=np.load("Dataset/flowers/%s_y.npy"%(classes[0]))
    print(X.shape)
    forflowerinclasses[1:]:
        X_aux=np.load("Dataset/flowers/%s_x.npy"%(flower))
        y_aux=np.load("Dataset/flowers/%s_y.npy"%(flower))
        print(X_aux.shape)
        X=np.concatenate((X,X_aux),axis=0)
        y=np.concatenate((y,y_aux),axis=0)
    
    print(X.shape)
    print(y.shape)
    
  2. 输出数据集中的一些样本:

    import random 
    random.seed(42) 
    from matplotlib import pyplot as plt
    import cv2
    for idx in range(5): 
        rnd_index = random.randint(0, 4000) 
        plt.subplot(1,5,idx+1),plt.imshow(cv2.cvtColor(X[rnd_index],cv2.COLOR_BGR2RGB)) 
        plt.xticks([]),plt.yticks([])
        plt.savefig("flowers_samples.jpg", bbox_inches='tight')
    plt.show() 
    

    输出如下所示:

    图 5.19:数据集中的样本

    图 5.23:数据集中的样本
  3. 现在,我们将进行归一化并执行一热编码:

    from keras import utils as np_utils
    X = (X.astype(np.float32))/255.0 
    y = np_utils.to_categorical(y, len(classes))
    print(X.shape)
    print(y.shape)
    
  4. 分割训练集和测试集:

    from sklearn.model_selection import train_test_split
    x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    input_shape = x_train.shape[1:]
    print(x_train.shape)
    print(y_train.shape)
    print(x_test.shape)
    print(y_test.shape)
    print(input_shape)
    
  5. 导入库并构建 CNN:

    from keras.models import Sequential
    from keras.callbacks import ModelCheckpoint
    from keras.layers import Input, Dense, Dropout, Flatten
    from keras.layers import Conv2D, Activation, BatchNormalization
    def CNN(input_shape):
        model = Sequential()
    
        model.add(Conv2D(32, kernel_size=(5, 5), padding='same',  strides=(2,2), input_shape=input_shape))
        model.add(Activation('relu')) 
        model.add(BatchNormalization()) 
        model.add(Dropout(0.2))
        model.add(Conv2D(64, kernel_size=(3, 3), padding='same', strides=(2,2))) 
        model.add(Activation('relu')) 
        model.add(BatchNormalization()) 
        model.add(Dropout(0.2))
        model.add(Conv2D(128, kernel_size=(3, 3), padding='same', strides=(2,2))) 
        model.add(Activation('relu')) 
        model.add(BatchNormalization()) 
        model.add(Dropout(0.2))
    
        model.add(Conv2D(256, kernel_size=(3, 3), padding='same', strides=(2,2))) 
        model.add(Activation('relu')) 
        model.add(BatchNormalization()) 
        model.add(Dropout(0.2))
    
        model.add(Flatten())
        model.add(Dense(512))
        model.add(Activation('relu'))
        model.add(BatchNormalization())
        model.add(Dropout(0.5))
        model.add(Dense(5, activation = "softmax"))
        return model
    
  6. 声明 ImageDataGenerator:

    from keras.preprocessing.image import ImageDataGenerator
    datagen = ImageDataGenerator(
            rotation_range=10,
            zoom_range = 0.2,
            width_shift_range=0.2,
            height_shift_range=0.2,
            shear_range=0.1,
            horizontal_flip=True
            )
    
  7. 我们现在将训练模型:

    datagen.fit(x_train)
    model = CNN(input_shape)
    model.compile(loss='categorical_crossentropy', optimizer='Adadelta', metrics=['accuracy'])
    ckpt = ModelCheckpoint('Models/model_flowers.h5', save_best_only=True,monitor='val_loss', mode='min', save_weights_only=False) 
    //{…}##the detailed code can be found on Github##
    model.fit_generator(datagen.flow(x_train, y_train,
                                batch_size=32),
                        epochs=200,
                        validation_data=(x_test, y_test),
                        callbacks=[ckpt],
                        steps_per_epoch=len(x_train) // 32,
                        workers=4)
    
  8. 之后,我们将评估模型:

    from sklearn import metrics
    model.load_weights('Models/model_flowers.h5')
    y_pred = model.predict(x_test, batch_size=32, verbose=0)
    y_pred = np.argmax(y_pred, axis=1)
    y_test_aux = y_test.copy()
    y_test_pred = list()
    for i in y_test_aux:
        y_test_pred.append(np.argmax(i))
    //{…}
    ##the detailed code can be found on Github##
    print (y_pred)
    # Evaluate the prediction
    accuracy = metrics.accuracy_score(y_test_pred, y_pred)
    print('Acc: %.4f' % accuracy)
    
  9. 达到的准确率是91.68%

  10. 尝试使用未见过的数据测试模型:

    classes = ['daisy','dandelion','rose','sunflower','tulip']
    images = ['sunflower.jpg','daisy.jpg','rose.jpg','dandelion.jpg','tulip .jpg']
    model.load_weights('Models/model_flowers.h5')
    for number in range(len(images)):
        imgLoaded = cv2.imread('Dataset/testing/%s'%(images[number])) 
        img = cv2.resize(imgLoaded, (150, 150)) 
        img = (img.astype(np.float32))/255.0 
        img = img.reshape(1, 150, 150, 3)
        plt.subplot(1,5,number+1),plt.imshow(cv2.cvtColor(imgLoaded,cv2.COLOR_BGR2RGB)) 
        plt.title(np.argmax(model.predict(img)[0])) 
        plt.xticks([]),plt.yticks([]) 
    plt.show()
    

    输出将如下所示:

图 5.20:活动 1 中玫瑰的预测结果

图 5.24:活动 05 中玫瑰的预测结果

注意:

该活动的详细代码可以在 GitHub 上找到 - github.com/PacktPublishing/Artificial-Vision-and-Language-Processing-for-Robotics/blob/master/Lesson05/Activity05/Activity05.ipynb

第六章:机器人操作系统(ROS)

活动 6:模拟器和传感器

解决方案

  1. 我们从创建包和文件开始:

    cd ~/catkin_ws/src
    catkin_create_pkg activity1 rospy sensor_msgs
    cd  activity1
    mkdir scripts
    cd scripts
    touch observer.py
    touch movement.py
    chmod +x observer.py
    chmod +x movement.py
    
  2. 这是图像获取节点的实现:

    注意:

    #!/usr/bin/env python
    import rospy
    from sensor_msgs.msg import Image
    import cv2
    from cv_bridge import CvBridge
    class Observer:
        bridge = CvBridge()
        counter = 0
        def callback(self, data):
            if self.counter == 20:
                cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8")
                cv2.imshow('Image',cv_image)
                cv2.waitKey(1000)
                cv2.destroyAllWindows()
                self.counter = 0
            else:
                self.counter += 1
        def observe(self):
            rospy.Subscriber('/camera/rgb/image_raw', Image, self.callback)
            rospy.init_node('observer', anonymous=True)
            rospy.spin()
    if __name__ == '__main__':
        obs = Observer()
        obs.observe()
    

    如你所见,这个节点与练习 21中的发布者和订阅者节点非常相似。唯一的区别是:

  3. 使用计数器仅显示收到的二十张图像中的一张。

    我们输入1000 (ms)作为Key()参数,这样每张图像就会显示一秒钟。

    这是运动节点的实现:

    #!/usr/bin/env python
    import rospy
    from geometry_msgs.msg import Twist
    def move():
        pub = rospy.Publisher('/mobile_base/commands/velocity', Twist, queue_size=1)
        rospy.init_node('movement', anonymous=True)
        move = Twist()
        move.angular.z = 0.5
        rate = rospy.Rate(10)
        while not rospy.is_shutdown():
            pub.publish(move)
            rate.sleep()
    if __name__ == '__main__':
        try:
            move()
        except rospy.ROSInterruptException:
            pass
    
  4. 为了执行文件,我们将执行这里提到的代码。

    注意:

    cd ~/catkin_ws
    source devel/setup.bash
    roscore
    roslaunch turtlebot_gazebo turtlebot_world.launch
    rosrun activity1 observer.py
    rosrun activity1 movement.py
    
  5. 运行两个节点并检查系统功能。你应该看到机器人转动自己,同时展示它所看到的图像。这是执行的一个顺序:

    输出将如下所示:

图 6.9:活动节点执行的第一个顺序

图 6.10:活动节点执行的第一个顺序

图 6.10:活动节点执行的第二个顺序

图 6.11:活动节点执行的第二个顺序

图 6.11: 活动节点执行的第三个序列

图 6.12: 活动节点执行的第三个序列

注意:

输出看起来类似但不完全像图 6.10、6.11 和 6.12 中提到的那样。

恭喜!您已完成此活动,最终将会有一个类似于图 6.8、6.9 和 6.10 的输出。通过成功完成此活动,您已能够实现并使用让您订阅相机的节点,在虚拟环境中显示图像。您还学会了让机器人自转,以便查看这些图像。

第七章:构建基于文本的对话系统(聊天机器人)

活动 7:创建用于控制机器人的对话代理

解决方案

  1. 打开您的 Google Colab 界面。

  2. 为书创建一个文件夹,并从 Github 下载utilsresponsestraining文件夹,然后上传到该文件夹中。

  3. 导入驱动并按如下方式挂载它:

    from google.colab import drive
    drive.mount('/content/drive')
    

    注意

    每次使用新的协作者时,请将驱动器挂载到所需的文件夹。

  4. 第一次挂载驱动后,您需要按照 Google 提供的 URL 并在键盘上按 Enter 键来输入授权代码:图 7.24: 显示 Google Colab 授权步骤的图像

    图 7.28: 显示 Google Colab 授权步骤的图像
  5. 现在您已经挂载了驱动器,需要设置目录的路径。

    cd /content/drive/My Drive/C13550/Lesson07/Activity01
    

    注意:

    步骤 5 中提到的路径可能会根据您在 Google Drive 上的文件夹设置而变化。路径将始终以 cd /content/drive/My Drive/开头。

  6. 导入 chatbot_intro 文件:

    from chatbot_intro import *
    
  7. 定义 GloVe 模型:

    filename = '../utils/glove.6B.50d.txt.word2vec'
    model = KeyedVectors.load_word2vec_format(filename, binary=False)
    
  8. 列出响应和训练句子文件:

    intent_route = 'training/'
    response_route = 'responses/'
    intents = listdir(intent_route)
    responses = listdir(response_route)
    print("Documents: ", intents)
    

    图 7.25: 意图文档列表

    图 7.29: 意图文档列表
  9. 创建文档向量:

    doc_vectors = np.empty((0,50), dtype='f')
    for i in intents:
        with open(intent_route + i) as f:
            sentences = f.readlines()
        sentences = [x.strip() for x in sentences]
        sentences = pre_processing(sentences)
    doc_vectors= np.append(doc_vectors,doc_vector(sentences,model),axis=0)
    print("Shape of doc_vectors:",doc_vectors.shape)
    print(" Vector representation of backward.txt:\n",doc_vectors)
    

    7.26: doc_vectors 的形状

    7.30: doc_vectors 的形状
  10. 预测意图:

    user_sentence = "Look to the right"
    user_sentence = pre_processing([user_sentence])
    user_vector = doc_vector(user_sentence,model).reshape(100,)
    intent = intents[select_intent(user_vector, doc_vectors)]
    intent
    

7.27: 预测意图

7.31: 预测意图

恭喜!您已完成此活动。如果希望,您可以添加更多意图,并训练 GloVe 模型以获得更好的结果。通过创建包含所有代码的函数,您可以编程和开发 ROS 中的运动节点,从而命令您的机器人进行移动和转向。

第八章:使用 CNN 引导机器人进行对象识别

活动 8:在视频中进行多对象检测和识别

解决方案

  1. 挂载驱动器:

    from google.colab import drive
    drive.mount('/content/drive')
    cd /content/drive/My Drive/C13550/Lesson08/
    
  2. 安装库:

    pip3 install https://github.com/OlafenwaMoses/ImageAI/releases/download/2.0.2/imageai-2.0.2-py3-none-any.whl
    
  3. 导入库:

    from imageai.Detection import VideoObjectDetection
    from matplotlib import pyplot as plt
    
  4. 声明模型:

    video_detector = VideoObjectDetection()
    video_detector.setModelTypeAsYOLOv3()
    video_detector.setModelPath("Models/yolo.h5")
    video_detector.loadModel()
    
  5. 声明回调方法:

    color_index = {'bus': 'red', 'handbag': 'steelblue', 'giraffe': 'orange', 'spoon': 'gray', 'cup': 'yellow', 'chair': 'green', 'elephant': 'pink', 'truck': 'indigo', 'motorcycle': 'azure', 'refrigerator': 'gold', 'keyboard': 'violet', 'cow': 'magenta', 'mouse': 'crimson', 'sports ball': 'raspberry', 'horse': 'maroon', 'cat': 'orchid', 'boat': 'slateblue', 'hot dog': 'navy', 'apple': 'cobalt', 'parking meter': 'aliceblue', 'sandwich': 'skyblue', 'skis': 'deepskyblue', 'microwave': 'peacock', 'knife': 'cadetblue', 'baseball bat': 'cyan', 'oven': 'lightcyan', 'carrot': 'coldgrey', 'scissors': 'seagreen', 'sheep': 'deepgreen', 'toothbrush': 'cobaltgreen', 'fire hydrant': 'limegreen', 'remote': 'forestgreen', 'bicycle': 'olivedrab', 'toilet': 'ivory', 'tv': 'khaki', 'skateboard': 'palegoldenrod', 'train': 'cornsilk', 'zebra': 'wheat', 'tie': 'burlywood', 'orange': 'melon', 'bird': 'bisque', 'dining table': 'chocolate', 'hair drier': 'sandybrown', 'cell phone': 'sienna', 'sink': 'coral', 'bench': 'salmon', 'bottle': 'brown', 'car': 'silver', 'bowl': 'maroon', 'tennis racket': 'palevilotered', 'airplane': 'lavenderblush', 'pizza': 'hotpink', 'umbrella': 'deeppink', 'bear': 'plum', 'fork': 'purple', 'laptop': 'indigo', 'vase': 'mediumpurple', 'baseball glove': 'slateblue', 'traffic light': 'mediumblue', 'bed': 'navy', 'broccoli': 'royalblue', 'backpack': 'slategray', 'snowboard': 'skyblue', 'kite': 'cadetblue', 'teddy bear': 'peacock', 'clock': 'lightcyan', 'wine glass': 'teal', 'frisbee': 'aquamarine', 'donut': 'mincream', 'suitcase': 'seagreen', 'dog': 'springgreen', 'banana': 'emeraldgreen', 'person': 'honeydew', 'surfboard': 'palegreen', 'cake': 'sapgreen', 'book': 'lawngreen', 'potted plant': 'greenyellow', 'toaster': 'ivory', 'stop sign': 'beige', 'couch': 'khaki'}
    def forFrame(frame_number, output_array, output_count, returned_frame):
        plt.clf()
        this_colors = []
        labels = []
        sizes = []
        counter = 0
        for eachItem in output_count:
            counter += 1
            labels.append(eachItem + " = " + str(output_count[eachItem]))
            sizes.append(output_count[eachItem])
            this_colors.append(color_index[eachItem])
        plt.subplot(1, 2, 1)
        plt.title("Frame : " + str(frame_number))
        plt.axis("off")
        plt.imshow(returned_frame, interpolation="none")
        plt.subplot(1, 2, 2)
        plt.title("Analysis: " + str(frame_number))
        plt.pie(sizes, labels=labels, colors=this_colors, shadow=True, startangle=140, autopct="%1.1f%%")
        plt.pause(0.01)
    
  6. 运行 Matplotlib 和视频检测过程:

    plt.show()
    video_detector.detectObjectsFromVideo(input_file_path="Dataset/videos/street.mp4", output_file_path="output-video" ,  frames_per_second=20, per_frame_function=forFrame,  minimum_percentage_probability=30, return_detected_frame=True, log_progress=True)
    

    输出将如下帧所示:

    图 8.7: ImageAI 视频对象检测输出

图 8.7: ImageAI 视频对象检测输出

正如您所见,模型可以较为准确地检测到对象。现在您可以在第八章的根目录中看到包含所有对象检测结果的输出视频。

注意:

Dataset/videos文件夹中新增了一个视频——park.mp4。你可以使用刚才提到的步骤,也可以在这个视频中识别物体。

第九章:机器人计算机视觉

活动 9:机器人安保守卫

解决方案

  1. 在你的 catkin 工作空间中创建一个新的包来包含集成节点。使用此命令来包括正确的依赖项:

    cd ~/catkin_ws/
    source devel/setup.bash
    roscore
    cd src
    catkin_create_pkg activity1 rospy cv_bridge geometry_msgs image_transport sensor_msgs std_msgs
    
  2. 切换到包文件夹并创建一个新的scripts目录。然后,创建 Python 文件并使其可执行:

    cd activity1
    mkdir scripts
    cd scripts
    touch activity.py
    touch activity_sub.py
    chmod +x activity.py
    chmod +x activity_sub.py
    
  3. 这是第一个节点的实现:

    库导入:

    #!/usr/bin/env python
    import rospy
    import cv2
    import sys
    import os
    from cv_bridge import CvBridge, CvBridgeError
    from sensor_msgs.msg import Image
    from std_msgs.msg import String
    sys.path.append(os.path.join(os.getcwd(), '/home/alvaro/Escritorio/tfg/darknet/python/'))
    import darknet as dn
    

    注意

    class Activity():
        def __init__(self):
    

    节点、订阅者和网络初始化:

            rospy.init_node('Activity', anonymous=True)
            self.bridge = CvBridge()
            self.image_sub = rospy.Subscriber("camera/rgb/image_raw", Image, self.imageCallback)
            self.pub = rospy.Publisher('yolo_topic', String, queue_size=10)
            self.imageToProcess = None
            cfgPath =  "/home/alvaro/Escritorio/tfg/darknet/cfg/yolov3.cfg"
            weightsPath = "/home/alvaro/Escritorio/tfg/darknet/yolov3.weights"
            dataPath = "/home/alvaro/Escritorio/tfg/darknet/cfg/coco2.data"
            self.net = dn.load_net(cfgPath, weightsPath, 0)
            self.meta = dn.load_meta(dataPath)
            self.fileName = 'predict.jpg'
            self.rate = rospy.Rate(10)
    

    注意

        def imageCallback(self, data):
            self.imageToProcess = self.bridge.imgmsg_to_cv2(data, "bgr8")
    

    节点的主函数:

        def run(self): 
            print("The robot is recognizing objects")
            while not rospy.core.is_shutdown():
                if(self.imageToProcess is not None):
                    cv2.imwrite(self.fileName, self.imageToProcess)
    

    对图像进行预测的方法:

                    r = dn.detect(self.net, self.meta, self.fileName)
                    objects = ""
                    for obj in r:
                        objects += obj[0] + " "
    

    发布预测:

                    self.pub.publish(objects)
                    self.rate.sleep()
    

    程序入口:

    if __name__ == '__main__':
        dn.set_gpu(0)
        node = Activity()
        try:
            node.run()
        except rospy.ROSInterruptException:
            pass
    
  4. 这是第二个节点的实现:

    库导入:

    #!/usr/bin/env python
    import rospy
    from std_msgs.msg import String
    

    类定义:

    class ActivitySub():
        yolo_data = ""
    
        def __init__(self):
    

    节点初始化和订阅者定义:

            rospy.init_node('ThiefDetector', anonymous=True)
            rospy.Subscriber("yolo_topic", String, self.callback)
    
    

    获取已发布数据的回调函数:

        def callback(self, data):
            self.yolo_data = data
        def run(self):
            while True:
    

    如果检测到有人员在数据中,启动警报:

                if "person" in str(self.yolo_data):
                    print("ALERT: THIEF DETECTED")
                    break
    

    程序入口:

    if __name__ == '__main__':
        node = ActivitySub()
        try:
            node.run()
        except rospy.ROSInterruptException:
            pass
    
  5. 现在,你需要将目标设置为 scripts 文件夹:

    cd ../../
    cd ..
    cd src/activity1/scripts/
    
  6. 执行 movement.py 文件:

    touch movement.py
    chmod +x movement.py
    cd ~/catkin_ws
    source devel/setup.bash
    roslaunch turtlebot_gazebo turtlebot_world.launch
    
  7. 打开一个新的终端并执行命令以获取输出:

    cd ~/catkin_ws
    source devel/setup.bash
    rosrun activity1 activity.py
    cd ~/catkin_ws
    source devel/setup.bash
    rosrun activity1 activity_sub.py
    cd ~/catkin_ws
    source devel/setup.bash
    rosrun activity1 movement.py
    
  8. 同时运行这两个节点。以下是执行示例:

    Gazebo 情况:

图 9.16:活动的示例情况

图 9.16:活动的示例情况

第一个节点输出:

图 9.17:第一个活动节点输出

图 9.17:第一个活动节点输出

第二个节点输出:

图 9.18:第二个活动节点输出

图 9.18:第二个活动节点输出
posted @ 2025-07-13 15:43  绝不原创的飞龙  阅读(73)  评论(0)    收藏  举报