神经网络基础理论与简单实践

机器学习(Machine Learning, ML) 在了解神经网络之前,我们需要先知道机器学习和神经网络之间的关系。 机器学习是人工智能中的一个分支, 主要研究如何让计算机模拟人类的学习行为来获得知识和技能,在实践上就是通过让泛型算法(Generic Algorithm,和 C/C++ 中的泛型算法不是一个东西)自己在输入的数据上建立逻辑模型,并通过该模型来得到目标结论。 泛型算法能处理的问题取决于我们所定义好的输入/输出的数据格式,而处理问题的条件和逻辑,则由泛型算法通过对输入数据进行分析学习构建处理逻辑参数,构建后的逻辑也能适应非预设数据的处理。神经网络属于机器学习泛型算法中的一种实现方案。 根据泛型算法的学习方式,也可以将机器学习分为监督式学习、弱监督学习、半监督学习、非监督式学习、迁移学习、强化学习等。 人工智能中许多维度的分类都存在交叉关系,因此通常不会为任何一个方法论进行确切的归类。 神经网络结构 【概念】机器学习中的神经网络模型是一种模仿生物神经网络结构和功能的模型,因此也被称为人工神经网络或类神经网络。人工神经网络由多个人工神经元(处理单元)以及传递信号的链接形成拓扑结构,由于泛型算法能处理的问题取决于输入/输出的数据格式,因此神经网络基本会分为3个层: Input Layer:输入层,用于接收外界数据,节点数量根据输入的数据类型决定 Hidden Layer:隐含层,负责对输入层提供的数据进行信息处理、信息转化,通常这一层会有多个层次,每层会将处理结果向后面传递。 Output Layer:输出层,将隐含层提供的输出信息转化层最终结果,节点数量根据输出的结构类型决定 【生物知识点复习】生物神经元通常具有多个树突,树突用于接收信号,接收的信号在细胞体内整合产生阶梯性生电,而轴突用于传递信号,当细胞体的电位影响达到一定的阈值,则代表这个神经元被激活,激活的神经元会产生神经冲动(电脉冲),通过轴突传导出去,轴突尾端有许多和其他神经元树突产生连接的突触,电信号会通过这些突触传递给其他神经元,突触在一次突触事件中产生的电位幅度与突触的强度有关。 人工神经元也是模拟生物神经元的结构和特性,下面是一个典型的人工神经元结构,以及人工神经元和生物神经元中各个行为的对照表。 生物神经元 人工神经元 输入,即上一个神经元轴突传递过来的电信号 x𝑖 也就是下一个神经元的输出 y 突触强度 w𝑗 权重 树突接收到的电信号 x𝑖 * w𝑗 信号积累,阶段性生电 ∑(x𝑖 * w𝑗) 神经元激活 𝑓(·) 激活函数 轴突电信号传递 y 也就是下一个神经元的输入 x𝑖 线性回归 从上面的对照表可以看出,一个神经元对上一层的输入处理其实就是将各个输入值加权求和,本质上就是一个线性回归 𝑓(𝑥;𝑤) = 𝑤1𝑥1 + 𝑤2𝑥2 + ⋯ + 𝑤D𝑥D + 𝚋。线性回归模型是机器学习中最基础最广泛的模型,主要用于分析自变量和因变量之间的关系。 为什么使用的是线性回归呢? 线性回归可以用来描述自变量和因变量之间的关系,机器学习中大部分问题都是分析数据里特征的关系来建立模型,因此在机器学习中很多问题都可以转换为线性回归问题来处理。 例如我们有如下左图关于面积和房价关系的数据,那么可以用一个一元线性回归模型来拟合这些数据,从而得到一个可以根据面积来预估房价的模型。当房屋特征变多时,也可以根据回归函数的参数建立一个没有 Hidden Layer 的神经网络。 面积和房价的线性拟合 多个房屋特征的神经网络 有时候特征向量与因变量并不是简单的线性关系,例如我们给的不是面积,而是房子的长宽,那么将会存在特征之间相乘的计算逻辑。或者有时候特征向量之间可能也存在关系,例如加上时间维度来预测未来的房价,那么其他特征带来的效果可能会跟着时间变化发生变化。如果我们在各个线性回归的关系上再次建立回归模型,那么工作量和计算量将会特别高,但多层的神经网络来处理这个问题就非常方便。 神经网络本身是由许多节点组成的,每个节点的输入输出都可以认为是一次线性转化,因此可以认为神经网络会将问题分为多个子问题来处理,不同纬度的问题会被分到各个层级,同一纬度的子问题会被分到各个神经元。每个神经元利用线性回归来对输入数据的特征进行线性转换(这个过程也称为特征提取),将子问题分析结果反馈给下一层级(父问题)继续处理。 没有 Hidden Layer 的神经网络只能用于表示线性回归函数,但多层的网络则可以在线性回归上建立更高纬度的模型。下面是 The Number of Hidden Layers 中总结的常见层数体系结构的功能:...

April 13, 2023

Jetpack Compose 探索

Why compose Compose UI 的编写只需要 Kotlin,在遵循 Android 应用架构时,这样更有利于聚合 UI Elements 的代码,不需要去区分 Kotlin 代码和 xml 布局文件,在我看来这种方式更加容易采用 Android 架构指南去控制项目架构。 但从另一方面来讲,Compose 这种嵌套的 UI 组合方式会加深代码层次,因此开发过程中需要对 UI 上各个元素做更细的区分,以增加代码的可读性。另外如果状态使用没有处理好,也会对 Compose 的重组性能带来影响。 完善的声明式 UI Android View 系统设计的时候是遵循 OOP 的,虽然有 XML 可以帮我们减少下面这种命令式代码的使用,但这种声明式构建 + 命令式执行的缺点还是很明显,因为需要一个加载器把布局转化到业务逻辑代码中。 // 命令式 val parent = ViewGroup(); val node = View(); parent.addView(node); 如果按照理想的声明式 UI 编写方式去改造传统 View 系统,那呈现出的代码可能会包括下面两个特点: 节点的构建不应该有返回值 节点的连接不依赖于 API <LinearLayout> <TextView>Hello World</TextView> <MaterialButton android:onCLick="syaHi()">hi</MaterialButton> </LinearLayout> 将上面的布局代码转换为 Java/Kotlin 理想的声明式代码 LinearLayout { TextView("Hello World") MaterialButton("Hi") { syaHi() } } Compose 利用 Kotlin DSL 构建声明式 UI,一个 @Composable 相当于一个节点,在内部也可以调用其他 @Composable 函数构建子节点。...

July 8, 2022

Flutter 绘制流程分析与代码实践

Render Tree 的创建过程 RenderObject 的类型 我们知道 Element 主要分为负责渲染的 RenderObjectElement 和负责组合的 ComponentElement 两大类,而创建 RenderObject 节点的是前者 mount() 方法中调用的 RenderObjectWidget.createRenderObject() 方法。 该方法是一个抽象方法,需要子类实现,对于不同的布局的 Widget 创建的 RenderObject 类型也不一样,在 Render Tree 中最主要的有两种 RenderObject: 首先是在 RenderObject 注释说明中大量提到了一个类 RenderBox,它是大部分的 RenderObjectWidget 所对应的 RenderObject 的抽象类 /// A render object in a 2D Cartesian coordinate system. /// 一个在 2D 坐标系中的渲染对象 abstract class RenderBox extends RenderObject 以及 Render Tree 的根节点 RenderView /// The root of the render tree. /// Render Tree 的根节点,处理渲染管道的引导和渲染树的输出 /// 它有一个填充整个输出表面的 RenderBox 类型的唯一子节点 class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> 其他的类型的 RenderObject 基本是为了特定布局(如滑动、列表)的实现,但大部分都直接或间接集成自 RenderBox。...

January 11, 2022

探索 Java & Kotlin 泛型

Kotlin 泛型基础 泛型可以让我们在代码中声明类型参数,Kotlin 泛型最基本的使用和 Java 一样,可以声明在类上和函数上,用法也都差不多。 声明在函数上时,可将类型参数作为参数或返回值的类型,该函数为泛型函数 声明在类上时,可以用在任意一处类型声明处,该类为泛型类 class GenericsDemo<T>(t: T) { val value = t } fun <T> invoke(t: T) : T { return t } 我们可以在声明了类型参数的类中,声明一个泛型方法,但如果内部方法所声明的类型参数名称和类上所声明的相同,那么会覆盖类上所声明的类型参数。下面的代码不会报错,并会打印 Hello 字符串。 class GenericsDemo<T>() { fun <T> invoke(t: T) : T { return t } } val demo = GenericsDemo<Int>() println(demo.invoke("Hello")) 此外,我们知道在类中可通过重载来定义同名方法,但这在泛型中并不起作用,如果类中拥有以下两个方法,那么将会报错。 class GenericsDemo<T>() { // 泛型来自类 fun invoke(t: T) : T { return t } // 泛型来自方法本身 fun <S> invoke(s: S) : S { return s } } 上诉代码报错原因是因为两个方法拥有相同的 signature,也就是在 JVM 看来这两个方法的方法名和参数都是一样的,报错信息如下:...

November 6, 2021

从源码角度分析 ThreadLocal 的使用

Java 中的创建的对象存放在堆内存,这一块空间是线程共享的,通常我们定义的变量所持有的是对象的引用存,即每个线程访问该变量时都将获取到堆内存中的同一个对象,因此在使用多线程的时候如果需要操作同一资源,那么需要思考线程安全的问题。 // ThreadDemo.java static int v = 0; static void autoAdd(){ for (int i = 0; i < 10000; i++) { v++; } } public static void main(String[] args) throws Exception { new Thread(ThreadLocalDemo::autoAdd).start(); autoAdd(); Thread.sleep(1000); System.out.println(v); } 上面代码变量 v 的打印结果可能会小于 20000,随着 v 自增次数的增加,例如循环次数增加到 100000,这个问题将会更加明显。 要解决线程安全的问题可以用同步或加锁的方式来处理,但有时候我们希望的该变量对于每个线程来说是独享的,也就是对于同一个变量,它的值对于不同线程都应该有一份单独的副本。 局部变量对于 Java 开发者而言应该是再熟悉不过的了。不同线程操作一个共享的变量时,我们可以创建一个新的局部变量来复制该变量的值,再进行操作,这样就不会污染到该共享变量的值。但对于复杂的对象而言,复制起来比较麻烦,而且这种方式也不能将操作完的值写回共享的变量。 虽然我们有办法解决这些问题,但没必要,因为 Java 提供了 ThreadLocal 来帮助我们创建一个线程局部的变量。 ThreadLocal 的使用 ThreadLocal 的使用利用了泛型,我们可以将期望的值放到 ThreadLocal 中,使用 ThreadLocal 时通过指定泛型类型来决定该 ThreadLocal 存放的数据的类型,这是集合类一样。 ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); System....

September 3, 2021

基于 JDK 1.8 分析 HashMap 的底层原理

Java 的 HashMap 可以说是用的最多、问的最多的一个 Map Collection 了。HashMap 是非同步的,即线程不安全。HashMap 允许存放的 key 为 null,但并不保证映射的顺序,也不保证这个顺序随时间保持不变。 本文对 HashMap 的代码分析基于 JDK 1.8 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 我们可以先从 HashMap 底层使用的数据结构了解 HashMap。 哈希表 + 链表 / 红黑树 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // ... } /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two....

May 31, 2021

基于 2-3-4 树理解红黑树的性质与操作

红黑树(Red-Black Tree)是一种自平衡的二叉查找树(Binary Search Tree, BST),由于基于二叉查找树(并不是基于 AVL 树),因此它是有序的。它出现于 1978 年 Leo J. Guibas 和 Robert Sedgewick 的一篇论文。 红黑树和 AVL 树很像,都是为了让二叉查找树能保持平衡,不会退化成链表,让查找时间复杂度能够稳定在 O(log(n))。 相比 AVL 树,红黑树牺牲了部分平衡性来,来减少插入 / 删除操作的旋转次数。因此插入性能红黑树会比 AVL 树快,但由于平衡性不如 AVL 树,当拥有相同数量的节点时,树的层数可能会比 AVL 树高,查询效率也不如 AVL 树。 由于红黑树的结构比较复杂,因此它也比较难理解,但我们可以借助 2-3-4 树来理解它。 Perfect Balance 的 2-3-4 树 介绍 2-3-4 树的资料可能比较少,由于 2-3-4 树的图画起来比较麻烦,为了偷懒本文选取了 Sedgewick 介绍 LLRB Tree(左倾红黑树) 的 PPT 中的一些图来做说明,该 PPT 链接放在本文末尾参考部分。 2-3-4 树也是一颗自平衡的树,但它的节点比较特殊,可以分为以下 3 种节点: 2-node:普通的树节点,可存放一个数据,最多有两个子节点 3-node:能存放两个数据的节点,最多能有三个子节点 4-node:能存放三个数据的节点,最多能有四个子节点 它们的结构图示如下(该图来自 Sedgewick 的 PPT 第 12 页): 该图中对子节点的连接位置进行描述的只有 3 节点,不过 4 节点与子节点连接方式其实也是一样的,子节点的连接决定于子节点存放的数据的大小范围和父节点的存放的数据的大小。...

May 2, 2021

被遗弃的 Vector 和 Stack

在 Java Collections Framework 中有两个被遗弃的 List 实现类 —— Vector 和 Stack。 Vector 通过实现 AbstractList<E> 接口来成为 Java Collections Framework List 接口的一员,而 Stack 直接继承于 Vector。 public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable public class Stack<E> extends Vector<E> 与 ArrayList 类似的 Vector 如果希望了解 ArrayList 的底层结构可阅读另一篇文章 ArrayList 与 LinkedList 底层结构 与 ArrayList 一样,Vector 的底层结构也是 Object 数组 elementData,通过 elementCount 来表示 Vector 存储的元素个数,但与 ArrayList 不同的是,ArrayList 创建时不指定容器个数时,elementData 是一个长度为 0 的数组,只有在第一次添加元素的时候才会创建一个长度为 10 的数组,而 Vector 则是在构造方法中调用另一个构造方法直接为 elementData 创建一个长度为 10 的数组。...

April 27, 2021