終於我用JOL打破了你對java對象的所有想象

目錄

  • 簡介
  • JOL簡介
  • 使用JOL分析VM信息
  • 使用JOL分析String
  • 使用JOL分析數組
  • 使用JOL分析自動裝箱
  • 使用JOL分析引用關係
  • 總結

簡介

使用面向對象的編程語言的好處就是,雖然沒有女朋友,但是仍然可以new對象出來。Java是面向對象的編程語言,我們天天都在使用java來new對象,但估計很少有人知道new出來的對象到底長的什麼樣子,是美是丑到底符不符合我們的要去?

對於普通的java程序員來說,可能從來沒有考慮過java中對象的問題,不懂這些也可以寫好代碼。

但是對於一個有鑽研精神的極客來說,肯定會想多一些,再多一些,java中的對象到底是什麼樣的。

今天,小F給大家介紹一款工具JOL,可以滿足大家對java對象的所有想象。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

更多內容請訪問www.flydean.com

JOL簡介

JOL的全稱是Java Object Layout。是一個用來分析JVM中Object布局的小工具。包括Object在內存中的佔用情況,實例對象的引用情況等等。

JOL可以在代碼中使用,也可以獨立的以命令行中運行。命令行的我這裏就不具體介紹了,今天主要講解怎麼在代碼中使用JOL。

使用JOL需要添加maven依賴:

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
</dependency>

添加完依賴,我們就可以使用了。

使用JOL分析VM信息

首先我們看下怎麼使用JOL來分析JVM的信息,代碼非常非常簡單:

log.info("{}", VM.current().details());

輸出結果:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

上面的輸出中,我們可以看到:Objects are 8 bytes aligned,這意味着所有的對象分配的字節都是8的整數倍。

使用JOL分析String

上面的都不是重點,重點是怎麼使用JOL來分成class和Instance信息。

其實java中的對象,除了數組,其他對象的大小應該都是固定的。我們先舉一個最最常用的字符串來看一下:

log.info("{}",ClassLayout.parseClass(String.class).toPrintable());

上面的例子中,我們使用ClassLayout來解析一個String類,先看下輸出:

[main] INFO com.flydean.JolUsage - java.lang.String object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     4    byte[] String.value                              N/A
     16     4       int String.hash                               N/A
     20     1      byte String.coder                              N/A
     21     1   boolean String.hashIsZero                         N/A
     22     2           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

先解釋下各個字段的含義,OFFSET是偏移量,也就是到這個字段位置所佔用的byte數,SIZE是後面類型的大小,TYPE是Class中定義的類型,DESCRIPTION是類型的描述,VALUE是TYPE在內存中的值。

分析下上面的輸出,我們可以得出,String類中佔用空間的有5部分,第一部分是對象頭,佔12個字節,第二部分是byte數組,佔用4個字節,第三部分是int表示的hash值,佔4個字節,第四部分是byte表示的coder,佔1個字節,最後一個是boolean表示的hashIsZero,佔1個字節,總共22個字節。但是JVM中對象內存的分配必須是8字節的整數倍,所以要補全2字節,最後String類的總大小是24字節。

有人可能要問小F了,如果字符串裏面存了很多很多數據,那麼對象的大小還是24字節嗎?

這個問題問得非常有水平,下面我們就來看看怎麼使用JOL來解析String對象的信息:

log.info("{}",ClassLayout.parseInstance("www.flydean.com").toPrintable());

上面的例子,我們使用了parseInstance而不是parseClass來解析String實例的信息。

輸出結果:

[main] INFO com.flydean.JolUsage - java.lang.String object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 c2 63 a2 (00000001 11000010 01100011 10100010) (-1570520575)
      4     4           (object header)                           0c 00 00 00 (00001100 00000000 00000000 00000000) (12)
      8     4           (object header)                           77 1a 06 00 (01110111 00011010 00000110 00000000) (399991)
     12     4    byte[] String.value                              [119, 119, 119, 46, 102, 108, 121, 100, 101, 97, 110, 46, 99, 111, 109]
     16     4       int String.hash                               0
     20     1      byte String.coder                              0
     21     1   boolean String.hashIsZero                         false
     22     2           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

先看結論,和String Class一樣,這個String對象確實只佔24字節。

實例的解析和Class解析的結果差不多,因為是實例對象,所以多了VALUE的值。

我們知道在JDK9之後,String的底層存儲從Char[] 變成了Byte[]用於節約String的存儲空間。上面的輸出中,我們可以看到String.value值確實很長,但是保存在String中的只是Byte數組的引用地址,所以4字節就夠了。

使用JOL分析數組

雖然String的大小是不變的,但是其底層數組的大小是可變的。我們再舉個例子:

log.info("{}",ClassLayout.parseClass(byte[].class).toPrintable());

輸出結果:

[main] INFO com.flydean.JolUsage - [B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    16        (object header)                           N/A
     16     0   byte [B.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

類的解析結果,可以看到Byte數組佔16個字節。

再看實例的情況:

log.info("{}",ClassLayout.parseInstance("www.flydean.com".getBytes()).toPrintable());

輸出結果:

[main] INFO com.flydean.JolUsage - [B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           22 13 07 00 (00100010 00010011 00000111 00000000) (463650)
     12     4        (object header)                           0f 00 00 00 (00001111 00000000 00000000 00000000) (15)
     16    15   byte [B.<elements>                             N/A
     31     1        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total

可以看到數組的大小真的變化了,這次變成了32字節。

使用JOL分析自動裝箱

我們知道,java中的基本類型都有一個和它對於的Object類型,比如long和Long,下面我們來分析下他們兩個在JVM中的內存區別:

log.info("{}",ClassLayout.parseClass(Long.class).toPrintable());

輸出結果:

[main] INFO com.flydean.JolUsage - java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到1個Long對象是佔24個字節的,但是其中真正存儲long的value只佔8個字節。

看一個實例:

log.info("{}",ClassLayout.parseInstance(1234567890111112L).toPrintable());

輸出結果:

[main] INFO com.flydean.JolUsage - java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           9a 15 00 00 (10011010 00010101 00000000 00000000) (5530)
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                1234567890111112
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

使用JOL分析引用關係

上面我們使用JOL分析的是class內部的空間使用情況,那麼如果有外部引用可不可以分析呢?

HashMap hashMap= new HashMap();
hashMap.put("flydean","www.flydean.com");
log.info("{}", GraphLayout.parseInstance(hashMap).toPrintable());

上面我們使用一個不同的layout:GraphLayout,它可以用來分析外部引用情況。

輸出結果:

[main] INFO com.flydean.JolUsage - java.util.HashMap@57d5872cd object externals:
          ADDRESS       SIZE TYPE                      PATH                           VALUE
        7875f9028         48 java.util.HashMap                                        (object)
        7875f9058         24 java.lang.String          .table[14].key                 (object)
        7875f9070         24 [B                        .table[14].key.value           [102, 108, 121, 100, 101, 97, 110]
        7875f9088         24 java.lang.String          .table[14].value               (object)
        7875f90a0         32 [B                        .table[14].value.value         [119, 119, 119, 46, 102, 108, 121, 100, 101, 97, 110, 46, 99, 111, 109]
        7875f90c0         80 [Ljava.util.HashMap$Node; .table                         [null, null, null, null, null, null, null, null, null, null, null, null, null, null, (object), null]
        7875f9110         32 java.util.HashMap$Node    .table[14]                     (object)

從結果我們可以看到HashMap本身是佔用48字節的,它裏面又引用了佔用24字節的key和value。

總結

使用JOL可以分析java類和對象,這個對於我們對JVM和java源代碼的理解和實現都是非常有幫助的。

本文的例子https://github.com/ddean2009/
learn-java-base-9-to-20

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/java-object-layout-jol/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)

上篇文章完成了文章增刪改的接口和友情鏈接列表的接口,本篇繼續。

善於思考的同學肯定發現,在執行增刪改操作后,Redis緩存中的數據還是存在的,也就意味着查詢接口返回的數據還是舊的,所以在寫接口之前,先完成一下清緩存的操作。

移除緩存

移除緩存我這裏找了一個新的包:Caching.CSRedis,選他是因為微軟的包Microsoft.Extensions.Caching.StackExchangeRedis沒有給我們實現批量刪除的功能。

Caching.CSRedis開源地址,https://github.com/2881099/csredis 在這不做過多介紹,感興趣的自己去看。

.Application.Caching層添加包Caching.CSRedisInstall-Package Caching.CSRedis,然後在模塊類MeowvBlogApplicationCachingModule中進行配置。

//MeowvBlogApplicationCachingModule.cs
...
public override void ConfigureServices(ServiceConfigurationContext context)
{
    ...

    var csredis = new CSRedis.CSRedisClient(AppSettings.Caching.RedisConnectionString);
    RedisHelper.Initialization(csredis);

    context.Services.AddSingleton<IDistributedCache>(new CSRedisCache(RedisHelper.Instance));
}
...

直接新建一個移除緩存的接口:ICacheRemoveService,添加移除緩存的方法RemoveAsync()。代碼較少,可以直接寫在緩存基類CachingServiceBase中。

public interface ICacheRemoveService
{
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key"></param>
    /// <param name="cursor"></param>
    /// <returns></returns>
    Task RemoveAsync(string key, int cursor = 0);
}

然後可以在基類中實現這個接口。

public async Task RemoveAsync(string key, int cursor = 0)
{
    var scan = await RedisHelper.ScanAsync(cursor);
    var keys = scan.Items;

    if (keys.Any() && key.IsNotNullOrEmpty())
    {
        keys = keys.Where(x => x.StartsWith(key)).ToArray();

        await RedisHelper.DelAsync(keys);
    }
}

簡單說一下這個操作過程,使用ScanAsync()獲取到所有的Redis key值,返回的是一個string數組,然後根據參數找到符合此前綴的所有key,最後調用DelAsync(keys)刪除緩存。

在需要有移除緩存功能的接口上繼承ICacheRemoveService,這裏就是IBlogCacheService

//IBlogCacheService.cs
namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService : ICacheRemoveService
    {
    }
}

在基類中已經實現了這個接口,所以現在所有繼承基類的緩存實現類都可以調用移除緩存方法了。

MeowvBlogConsts中添加緩存前綴的常量。

//MeowvBlogConsts.cs
/// <summary>
/// 緩存前綴
/// </summary>
public static class CachePrefix
{
    public const string Authorize = "Authorize";

    public const string Blog = "Blog";

    public const string Blog_Post = Blog + ":Post";

    public const string Blog_Tag = Blog + ":Tag";

    public const string Blog_Category = Blog + ":Category";

    public const string Blog_FriendLink = Blog + ":FriendLink";
}

然後在BlogService.Admin.cs服務執行增刪改后調用移除緩存的方法。

//BlogService.Admin.cs

// 執行清除緩存操作
await _blogCacheService.RemoveAsync(CachePrefix.Blog_Post);

因為是小項目,採用這種策略直接刪除緩存,這樣就搞定了當在執行增刪改操作后,前台接口可以實時查詢出最後的結果。

文章詳情

當我們修改文章數據的時候,是需要把當前數據庫中的數據帶出來显示在界面上的,因為有可能只是個別地方需要修改,所以這還需要一個查詢文章詳情的接口,當然這裏的詳情和前端的是不一樣的,這裡是需要根據Id主鍵去查詢。

添加模型類PostForAdminDto.cs,直接繼承PostDto,然後添加一個Tags列表就行,==,好像和上一篇文章中的EditPostInput字段是一模一樣的。順手將EditPostInput改一下吧,具體代碼如下:

//PostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostForAdminDto : PostDto
    {
        /// <summary>
        /// 標籤列表
        /// </summary>
        public IEnumerable<string> Tags { get; set; }
    }
}

//EditPostInput.cs
namespace Meowv.Blog.Application.Contracts.Blog.Params
{
    public class EditPostInput : PostForAdminDto
    {
    }
}

IBlogService.Admin.cs中添加接口。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync(int id);

實現這個接口。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync(int id)
{
    var result = new ServiceResult<PostForAdminDto>();

    var post = await _postRepository.GetAsync(id);

    var tags = from post_tags in await _postTagRepository.GetListAsync()
               join tag in await _tagRepository.GetListAsync()
               on post_tags.TagId equals tag.Id
               where post_tags.PostId.Equals(post.Id)
               select tag.TagName;

    var detail = ObjectMapper.Map<Post, PostForAdminDto>(post);
    detail.Tags = tags;
    detail.Url = post.Url.Split("/").Where(x => !string.IsNullOrEmpty(x)).Last();

    result.IsSuccess(detail);
    return result;
}

先根據Id查出文章數據,再通過聯合查詢找出標籤數據。

CreateMap<Post, PostForAdminDto>().ForMember(x => x.Tags, opt => opt.Ignore());

新建一條AutoMapper配置,將Post轉換成PostForAdminDto,忽略Tags。

然後將查出來的標籤、Url賦值給DTO,輸出即可。在BlogController.Admin中添加API。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Authorize]
[Route("admin/post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync([Required] int id)
{
    return await _blogService.GetPostForAdminAsync(id);
}

至此,完成了關於文章的所有接口。

接下來按照以上方式依次完成分類、標籤、友鏈的增刪改查接口,我覺得如果你有跟着我一起做,剩下的可以自己完成。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

Java併發編程實戰總結 (一)

前提

首先該場景是一個酒店開房的業務。為了朋友們閱讀簡單,我把業務都簡化了。
業務:開房後會添加一條賬單,添加一條房間排期記錄,房間排期主要是為了房間使用的時間不衝突。如:賬單A,使用房間1,使用時間段為2020-06-01 12:00 – 2020-06-02 12:00 ,那麼還需要使用房間1開房的時間段則不能與賬單A的時間段衝突。

業務類

為了簡單起見,我把幾個實體類都簡化了。

賬單類

public class Bill {
    // 賬單號
    private String serial;

    // 房間排期id
    private Integer room_schedule_id;
    // ...get set
}

房間類

// 房間類
public class Room {
    private Integer id;

    // 房間名
    private String name;
    // get set...
}

房間排期類

import java.sql.Timestamp;

public class RoomSchedule {
    private Integer id;
    
    // 房間id
    private Integer roomId;

    // 開始時間
    private Timestamp startTime;

    // 結束時間
    private Timestamp endTime;
    // ...get set
}

實戰

併發實戰當然少不了Jmeter壓測工具,傳送門: https://jmeter.apache.org/download_jmeter.cgi
為了避免有些小夥伴訪問不到官網,我上傳到了百度雲:鏈接:https://pan.baidu.com/s/1c9l3Ri0KzkdIkef8qtKZeA
提取碼:kjh6

初次實戰(sychronized)

第一次進行併發實戰,我是首先想到sychronized關鍵字的。沒辦法,基礎差。代碼如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import java.sql.Timestamp;

/**
 * 開房業務類
 */
@Service
public class OpenRoomService {
    @Autowired
    DataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
    TransactionDefinition transactionDefinition;

    public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) {
        // 開啟事務
        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            synchronized (RoomSchedule.class) {
                if (isConflict(roomId, startTime, endTime)) {
                    // throw exception
                }
                // 添加房間排期...
                // 添加賬單

                // 提交事務
                dataSourceTransactionManager.commit(transaction);
            }
        } catch (Exception e) {
            // 回滾事務
            dataSourceTransactionManager.rollback(transaction);
            throw e;
        }
    }

    public boolean isConflict(Integer roomId, Timestamp startTime, Timestamp endTime) {
        // 判斷房間排期是否有衝突...
    }
}
  1. sychronized(RoomSchedule.class),相當於的開房業務都是串行的。不管開房間1還是房間2。都需要等待上一個線程執行完開房業務,後續才能執行。這並不好哦。
  2. 事務必須在同步代碼塊sychronized中提交,這是必須的。否則當線程A使用房間1開房,同步代碼塊執行完,事務還未提交,線程B發現房間1的房間排期沒有衝突,那麼此時是有問題的。

錯誤點: 有些朋友可能會想到都是串行執行了,為什麼不把synchronized關鍵字寫到方法上?
首先openRoom方法是非靜態方法,那麼synchronized鎖定的就是this對象。而Spring中的@Service註解類是多例的,所以並不能把synchronized關鍵字添加到方法上。

二次改進(等待-通知機制)

因為上面的例子當中,開房操作都是串行的。而實際情況使用房間1開房和房間2開房應該是可以并行才對。如果我們使用synchronized(Room實例)可以嗎?答案是不行的。
在第三章 解決原子性問題當中,我講到了使用鎖必須是不可變對象,若把可變對象作為鎖,當可變對象被修改時相當於換鎖,這裏的鎖講的就是synchronized鎖定的對象,也就是Room實例。因為Room實例是可變對象(set方法修改實例的屬性值,說明為可變對象),所以不能使用synchronized(Room實例)
在這次改進當中,我使用了第五章 等待-通知機制,我添加了RoomAllocator房間資源分配器,當開房的時候需要在RoomAllocator當中獲取鎖資源,獲取失敗則線程進入wait()等待狀態。當線程釋放鎖資源則notiryAll()喚醒所有等待中的線程。
RoomAllocator房間資源分配器代碼如下:

import java.util.ArrayList;
import java.util.List;

/**
 * 房間資源分配器(單例類)
 */
public class RoomAllocator {
    private final static RoomAllocator instance = new RoomAllocator();

    private final List<Integer> lock = new ArrayList<>();

    private RoomAllocator() {}

    /**
     * 獲取鎖資源
     */
    public synchronized void lock(Integer roomId) throws InterruptedException {
        // 是否有線程已佔用該房間資源
        while (lock.contains(roomId)) {
            // 線程等待
            wait();
        }

        lock.add(roomId);
    }

    /**
     * 釋放鎖資源
     */
    public synchronized void unlock(Integer roomId) {
        lock.remove(roomId);
        // 喚醒所有線程
        notifyAll();
    }

    public static RoomAllocator getInstance() {
        return instance;
    }
}

開房業務只需要修改openRoom的方法,修改如下:

    public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) throws InterruptedException {
        RoomAllocator roomAllocator = RoomAllocator.getInstance();
        // 開啟事務
        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            roomAllocator.lock(roomId);
            if (isConflict(roomId, startTime, endTime)) {
                // throw exception
            }
            // 添加房間排期...
            // 添加賬單

            // 提交事務
            dataSourceTransactionManager.commit(transaction);
        } catch (Exception e) {
            // 回滾事務
            dataSourceTransactionManager.rollback(transaction);
            throw e;
        } finally {
            roomAllocator.unlock(roomId);
        }
    }

那麼此次修改后,使用房間1開房和房間2開房就可以并行執行了。

總結

上面的例子可能會有其他更好的方法去解決,但是我的實力不允許我這麼做….。這個例子也是我自己在項目中搞事情搞出來的。畢竟沒有實戰經驗,只有理論,不足以學好併發。希望大家也可以在項目中搞事情[壞笑],當然不能瞎搞。
後續如果在其他場景用到了併發,也會繼續寫併發實戰的文章哦~

個人博客網址: https://colablog.cn/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(1)——SVM概述,Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(4)——sklearn實現

  上一節我學習了完整的SVM過程,下面繼續對核函數進行詳細學習,具體的參考鏈接都在上一篇文章中,SVM四篇筆記鏈接為:

Python機器學習筆記:SVM(1)——SVM概述

Python機器學習筆記:SVM(2)——SVM核函數

Python機器學習筆記:SVM(3)——證明SVM

Python機器學習筆記:SVM(4)——sklearn實現

  熱身實例

  我在上一節有完整的學習了SVM算法,為了不讓自己這麼快就忘了,這裏先學習一個實例,回顧一下,並引出核函數的概念。

  數據是這樣的:有三個點,其中正例 x1(3,  3),x2(4,3),負例 x3(1,1)

   求解:

   約束條件為:

  這個約束條件是通過這個得到的(為什麼這裏強調一下呢,因為我們這個例子本身說的就是SVM的求解過程):

   我們可以知道  y1 = +1, y2 = +1 , y3 = -1,同時,代入 α ,則得到:

  α1 +  α2 – α3=0

  下面通過SVM求解實例。

  我們將數據代入原式中:

   由於 α1 + α2 = α3 化簡可得:

   然後分別對  α1 和 α2 求偏導,偏導等於 0 可得: α1 = 1.5   α2 = -1 ,但是我們發現這兩個解並不滿足約束條件  αi >= 0,i=1,2,3,所以解應該在邊界上(正常情況下,我們需要對上式求偏導,進而算出 w,b)。

  首先令  α1 = 0,得出 α2 = -2/13  ,α3 = -2/13   (不滿足約束)

  再令 α2 = 0,得出 α1 = 0.25  ,α3 = 0.25  (滿足約束)

  所以最小值在(0.25, 0,0.25)處取得。

  我們將 α 的結果代入求解:

   所以我們代入 w  b ,求得最終的平面方程為:

  熱身完畢,下面學習核函數,為了方便理解我們下面要說的核函數,我在知乎找了一個簡單易懂的故事,讓我們了解支持向量機,更是明白支持向量機的核函數是個什麼鬼,下面看故事。

1,故事分析:支持向量機(SVM)是什麼?

  下面故事來源於此(這是源作者鏈接):點擊我即可

  在很久以前的情人節,有一個大俠要去救他的愛人,但是魔鬼和他玩了一個遊戲。

  魔鬼在桌面上似乎有規律放了兩種顏色的球,說:“你用一根棍分開他們?要求:盡量在放更多球之後,仍然使用”

  於是大俠這樣做,幹得不錯吧:

  然後魔鬼又在桌上放了更多的球,似乎有一個球站錯了陣營。

  SVM就是試圖把棍放在最佳位置,好讓在棍的兩邊有盡可能大的間隙。

  現在即使魔鬼放了更多的球,棍仍然是一個好的分界線。

  然後,在SVM工具箱中有另一個更加重要的trick 。魔鬼看到大俠已經學會了一個trick,於是魔鬼給了大俠一個新的挑戰。

  現在,大俠沒有棍可以很好地幫他分開這兩種球了,現在怎麼辦,當然像所有武俠片中一樣大俠桌子一拍,球飛到空中。然後憑藉著大俠的輕功,大俠抓起一張紙,插到了兩種球的中間。

  現在,從魔鬼的角度看這些球,這些球好像是被一條曲線分開了。

  在之後,無聊的大人們,把這些球叫做 「data」,把棍子 叫做 「classifier」, 最大間隙trick 叫做「optimization」, 拍桌子叫做「kernelling」, 那張紙叫做「hyperplane」。

  所以說,Support Vector Machine,一個普通的SVM就是一條直線罷了,用來完美劃分linearly separable的兩類。但是這又不是一條普通的直線,這是無數條可以分類的直線當中最完美的,因為它恰好在兩個類的中間,距離兩個類的點都一樣遠。而所謂的Support vector就是這些離分界線最近的點,如果去掉這些點,直線多半是要改變位置的。如果是高維的點,SVM的分界線就是平面或者超平面。其實沒有差,都是一刀切兩塊,我們這裏統一叫做直線。

  再理解一下,當一個分類問題,數據是線性可分的,也就是用一根棍就可以將兩種小球分開的時候,我們只要將棍的位置放在讓小球距離棍的距離最大化的位置即可。尋找這個最大間隔的過程,就叫做最優化。但是,显示往往是殘酷的,一般的數據是線性不可分的。也就是找不到一個棍將兩種小球很好的分類,這時候我們就需要像大俠一樣,將小球排起,用一張紙代替小棍將兩種小球進行分類,想讓數據飛起,我們需要的東西就是核函數(kernel),用於切分小球的紙,就是超平面。

2,核函數的概念

  上面故事說明了SVM可以處理線性可分的情況,也可以處理非線性可分的情況。而處理非線性可分的情況是選擇了核函數(kernel),通過將數據映射到高位空間,來解決在原始空間中線性不可分的問題。

  我們希望樣本在特徵空間中線性可分,因此特徵空間的好壞對支持向量機的性能至關重要,但是在不知道特徵映射的情況下,我們是不知道什麼樣的核函數是適合的,而核函數也只是隱式的定義了特徵空間,所以,核函數的選擇對於一個支持向量機而言就顯得至關重要,若選擇了一個不合適的核函數,則數據將映射到不合適的樣本空間,從而支持向量機的性能將大大折扣。

  所以構造出一個具有良好性能的SVM,核函數的選擇是關鍵。而核函數的選擇包含兩部分工作:一是核函數類型的選擇,二是確定核函數類型后相關參數的選擇。

  我們知道,核函數的精妙之處在於不用真的對特徵向量做核映射,而是直接對特徵向量的內積進行變換,而這種變換卻等價於先對特徵向量做核映射然後做內積。

  SVM主要是在用對偶理論求解一個二次凸優化問題,其中對偶問題如下:

   求得最終結果:

  當然這是線性可分的情況,那麼如果問題本身是線性不可分的情況呢?那就是先擴維后再計算。具體來說,在線性不可分的情況下,支持向量機首先在低維空間中完成計算,然後通過核函數將輸入空間映射到高維特徵空間,最終在高維特徵空間中構造出最優分離超平面,從而把平面上本身不好分的非線性數據分開。如下圖所示,一堆數據在二維空間中無法劃分,從而映射到三維空間中劃分:

   而在我們遇到核函數之前,如果用原始的方法,那麼在用線性學習器學習一個非線性關係,需要選擇一個非線性特徵集,並且將數據寫成新的表達形式,這等價於應用一個固定的非線性映射,將數據映射到特徵空間,在特徵空間中使用線性學習器。因此,考慮的假設集是這種類型的函數:

   這裏 Φ :X -> F 是從輸入空間到某個特徵空間的映射,這意味着建立非線性學習器分為兩步:

  • 1,使用一個非線性映射將數據變換到一個特徵空間 F
  • 2,在特徵空間使用線性學習器分類

  而由於對偶形式就是線性學習器的一個重要性質,這意味着假設可以表達為訓練點的線性組合,因此決策規則可以用測試點和訓練點的內積來表示:

  為向量加上核映射后,要求解的最優化問題變為:

  根據核函數滿足的等式條件,它等價於下面的問題:

  其線性不可分情況的對偶形式如下:

   其中 Φ(xi) 表示原來的樣本擴維后的坐標。

  最後得到的分類判別函數為:

   和不用核映射相比,只是求解的目標函數,最後的判定函數對特徵向量的內積做了核函數變換。如果K是一個非線性函數,上面的決策函數則是非線性函數,此時SVM是非線性模型。當訓練樣本很多,支持向量的個數很大的時候,預測時的速度是一個問題,因此很多時候我們會使用線性支持向量機。

3,舉例說明核函數的巧妙之處

  下面先從一個小例子來闡述問題。假設我們有兩個數據, x = (x1,  x2,  x3)  y = (y1,  y2,  y3)。此時在3D空間已經不能對其進行線性劃分了,那麼我們通過一個函數將數據映射到更高維的空間,比如9維的話,那麼 f(x) = (x1x1, x1x2, x1x3, x2x1, x2x2, x2x3, x3x1, x3x2, x3x3),由於需要計算內積,所以在新的數據在 9 維空間,需要計算  <f(x),  f(y)> 的內積,需要花費 O(n^2)。

  再具體點,令 x = (1, 2, 3), y = (4, 5, 6),那麼 f(x)  = (1, 2, 3, 2, 4, 6, 3, 6, 9),f(y) = (16, 20, 24, 20, 25, 36, 24, 30, 36)

  此時: <f(x),  f(y)>  = 16 + 40 + 72 +40 +100 + 180 + 72 +180 +324 = 1024

  對於3D空間這兩個數據,似乎還能計算,但是如果將維數擴大到一個非常大數的時候,計算起來可就不是這麼一點點問題了。

  然而,我們發現  K(x, y) = (<x, y>)^2   ,代入上式: K(x, y) = (4 + 10 + 18)^2 = 32^2 = 1024

  也就是說 : K(x, y) = (<x, y>)^2  = <f(x),  f(y)>

  但是 K(x, y) 計算起來卻比 <f(x), f(y)> 簡單的多,也就是說只要用 K(x, y)來計算,效果與 <f(x), f(y)> 是一樣的,但是計算效率卻大幅度提高了,如  K(x, y) 是 O(n),而  <f(x), f(y)> 是 O(n^2),所以使用核函數的好處就是,可以在一個低維空間去完成一個高緯度(或者無限維度)樣本內積的計算,比如上面例子中 K(x, y)的3D空間對比 <f(x), f(y)> 的9D空間。

  下面再舉個例子來證明一下上面的問題,為了簡單起見,假設所有樣本點都是二維點,其值分別為(x,  y),分類函數為:

   它對應的映射方式為:

   可以驗證:任意兩個擴維后的樣本點在3維空間的內積等於原樣本點在二維空間的函數輸出

   有了這個核函數,以後的高維內積都可以轉換為低維的函數運算了,這裏也就是說只需要計算低維的內積,然後再平方。明顯問題得到解決且複雜度降低極大。總而言之:核函數它本質上隱含了從低維到高維的映射,從而避免直接計算高維的內積

  當然上述例子是多項式核函數的一個特例,其實核函數的種類還有很多,後文會一一介紹。

4,核函數的計算原理

  通過上面的例子,我們大概可以知道核函數的巧妙應用了,下面學習一下核函數的計算原理。

  如果有一種方法可以在特徵空間中直接計算內積  <Φ(xi , Φ(x)> ,就像在原始輸入點的函數中一樣,就有可能將兩個步驟融合到一起建立一個非線性的學習器,這樣直接計算的方法稱為核函數方法。

  設 x 是輸入空間(歐式空間或者離散集合),H為特徵空間(希爾伯特空間),如果存在一個從 x 到 H 的映射:

  核是一個函數 K,對於所有 x, z ∈ χ, 則滿足:

   則稱Κ(x,z)為核函數,φ(x)為映射函數,φ(x)∙φ(z)為x,z映射到特徵空間上的內積。

  參考網友的理解:任意兩個樣本點在擴維后的空間的內積,如果等於這兩個樣本點在原來空間經過一個函數后的輸出,那麼這個函數就叫核函數

  由於映射函數十分複雜難以計算,在實際中,通常都是使用核函數來求解內積,計算複雜度並沒有增加,映射函數僅僅作為一種邏輯映射,表徵着輸入空間到特徵空間的映射關係。至於為什麼需要映射后的特徵而不是最初的特徵來參与計算,為了更好地擬合是其中一個原因,另外的一個重要原因是樣例可能存在線性不可分的情況,而將特徵映射到高維空間后,往往就可分了。

  下面將核函數形式化定義。如果原始特徵內積是 <X ,  Z>,映射 <Φ(xi • Φ(x)>,那麼定義核函數(Kernel)為:

  到這裏,我們可以得出結論,如果要實現該節開頭的效果,只需要計算 Φ(x) ,然後計算 Φ(x)TΦ(x)即可,然而這種計算方式是非常低效的。比如最初的特徵是n維的,我們將其映射到 n2 維,然後再計算,這樣需要O(n2 ) 的時間,那麼我們能不能想辦法減少計算時間呢?

  先說結論,當然是可以的,畢竟我們上面例子,活生生的說明了一個將需要 O(n2 ) 的時間 轉換為 需要O(n ) 的時間。

  先看一個例子,假設x和z都是n維度的,

  展開后,得到:

  這個時候發現我們可以只計算原始特徵 x 和 z 內積的平方(時間複雜度為O(n)),就等價於計算映射后特徵的內積。也就是說我們不需要花O(n2 ) 的時間了。

  現在看一下映射函數(n = 3),根據上面的公式,得到:

  也就是說核函數  Κ(x,z) = (xTz)2  只能選擇這樣的 φ 作為映射函數時才能夠等價於映射后特徵的內積

  再看另外一個核函數,高斯核函數:

  這時,如果 x 和 z 很相近 (||x – z || 約等於 0),那麼核函數值為1,如果 x 和 z 相差很大(||x – z ||  >> 0),那麼核函數值約等於0.由於這個函數類似於高斯分佈,因此稱為高斯核函數,也叫做徑向基函數(Radial Basis Function 簡稱為RBF)。它能夠把原始特徵映射到無窮維。

  下面有張圖說明在低維線性不可分時,映射到高維后就可分了,使用高斯核函數。

  注意,使用核函數后,怎麼分類新來的樣本呢?線性的時候我們使用SVM學習出w和b,新來樣本x的話,我們使用 wTx + b 來判斷,如果值大於等於1,那麼是正類,小於等於是負類。在兩者之間,認為無法確定。如果使用了核函數后,wTx + b 就變成了 wTΦ(x) + b,是否先要找到 Φ(x) ,然後再預測?答案肯定不是了,找 Φ(x) 很麻煩,回想我們之前說過的。

  只需將 <(x(i) , x> 替換成  (x(i) , x),然後值的判斷同上。

4.1  核函數有效性的判定

  問題:給定一個函數K,我們能否使用K來替代計算 Φ(x)TΦ(x),也就說,是否能夠找出一個 Φ,使得對於所有的x和z,都有 K(x, z) = Φ(x)TΦ(x),即比如給出了 K(x, z) = (xTz)2,是否能夠認為K是一個有效的核函數。

  下面來解決這個問題,給定m個訓練樣本(x(1),x(2), ….,x(m)),每一個x(i) 對應一個特徵向量。那麼,我們可以將任意兩個 x(i) 和 x(j) 帶入K中,計算得到Kij = K(x(i), x(j))。i 可以從1到m,j 可以從1到m,這樣可以計算出m*m的核函數矩陣(Kernel Matrix)。為了方便,我們將核函數矩陣和 K(x, z) 都使用 K 來表示。如果假設 K 是有效地核函數,那麼根據核函數定義:

  可見,矩陣K應該是個對稱陣。讓我們得出一個更強的結論,首先使用符號ΦK(x)來表示映射函數 Φ(x) 的第 k 維屬性值。那麼對於任意向量 z,得:

  最後一步和前面計算 K(x, z) = (xTz)2 時類似。從這個公式我們可以看出,如果K是個有效的核函數(即 K(x, z)   Φ(x)TΦ(z)等價),那麼,在訓練集上得到的核函數矩陣K應該是半正定的(K>=0)。這樣我們得到一個核函數的必要條件:K是有效的核函數 ==> 核函數矩陣K是對稱半正定的。

  Mercer定理表明為了證明K是有效的核函數,那麼我們不用去尋找 Φ ,而只需要在訓練集上求出各個 Kij,然後判斷矩陣K是否是半正定(使用左上角主子式大於等於零等方法)即可。

 

5,核函數:如何處理非線性數據

  來看個核函數的例子。如下圖所示的兩類數據,分別分佈為兩個圓圈的形狀,這樣的數據本身就是線性不可分的,此時我們該如何把這兩類數據分開呢?

   事實上,上圖所示的這個數據集,是用兩個半徑不同的圓圈加上了少量的噪音生成得到的,所以,一個理想的分界應該是“圓圈” 而不是“一條線”(超平面)。如果用 X1 和 X2 來表示這個二維平面的兩個坐標的話,我們知道一條二次曲線(圓圈是二次曲線的一種特殊情況)的方程可以寫作這樣的形式:

   注意上面的形式,如果我們構造另外一個五維的空間,其中五個坐標的值分別為:

   那麼顯然,上面的方程在新的坐標系下可以寫做:

   關於新的坐標 Z,這正是一個 hyper plane 的方程!也就是說,如果我們做一個映射:

   將X按照上面的規則映射為 Z,那麼在新的空間中原來的數據將變成線性可分的,從而使用之前我們推導的線性分類算法就可以進行處理了。這正是Kernel方法處理非線性問題的基本思想。

  再進一步描述 Kernel 的細節之前,不妨再來看看上述例子在映射過後的直觀形態。當然,我們無法將五維空間畫出來,不過由於我這裏生成數據的時候用了特殊的情形,所以這裏的超平面實際的方程是這個樣子的(圓心在X2軸上的一 個正圓):

   因此我只需要把它映射到下面這樣一個三維空間中即可:

   下圖即是映射之後的結果,將坐標軸經過適當的旋轉,就可以很明顯的看出,數據是可以通過一個平面來分開的

  核函數相當於把原來的分類函數:

   映射成:

   而其中的 α 可以通過求解如下 dual 問題而得到的:

   這樣一來問題就解決了嗎?似乎是的:拿到非線性數據,就找一個映射(Φ(•),然後一股腦把原來的數據映射到新空間中,再做線性SVM即可。不過事實上問題好像沒有這麼簡單)。

  細想一下,剛才的方法是不是有問題:

  在最初的例子里,我們對一個二維空間做映射,選擇的新空間是原始空間的所有一階和二階的組合,得到了五個維度;

  如果原始空間是三維(一階,二階和三階的組合),那麼我們會得到:3(一次)+3(二次交叉)+3(平方)+3(立方)+1(x1 * x2 * x3) + 2*3(交叉,一個一次一個二次,類似 x1*x2^2)=19 維的新空間,這個數目是呈指數級爆炸性增長的,從而勢必這給 Φ(•) 的計算帶來非常大的困難,而且如果遇到無窮維的情況,就根本無從計算了。

  這個時候,可能就需要Kernel出馬了。

  不妨還是從最開始的簡單例子觸發,設兩個向量為:

   而 Φ(•) 即是前面說的五維空間的映射,因此映射過後的內積為:

   (公式說明:上面的這兩個推導過程中,所說的前面的五維空間的映射,這裏說的便是前面的映射方式,回顧下之前的映射規則,再看看那個長的推導式,其實就是計算x1,x2各自的內積,然後相乘相加即可,第二個推導則是直接平方,去掉括號,也很容易推出來)

  另外,我們又注意到:

   二者有很多相似的地方,實際上,我們只要把某幾個維度線性縮放一下,然後再加上一個常數維度,具體來說,上面這個式子的計算結果實際上和映射

   之後的內積  <Φ(xi • Φ(x)>  的結果是相等的,那麼區別在什麼地方呢?

  • 1,一個是映射到高維空間中,然後再根據內積的公式進行計算
  • 2,另一個則直接在原來的低維空間中進行計算,而不需要顯式地寫出映射后的結果

  (公式說明:上面之中,最後的兩個式子,第一個算式,是帶內積的完全平方式,可以拆開,然後,再通過湊一個得到,第二個算式,也是根據第一個算式湊出來的)

  回想剛才提到的映射的維度爆炸,在前一種方法已經無法計算的情況下,后一種方法卻依舊能從容處理,甚至是無窮維度的情況也沒有問題。

  我們把這裏的計算兩個向量在隱式映射過後的空間中的內積的函數叫做核函數(kernel Function),例如,在剛才的例子中,我們的核函數為:

   核函數能簡化映射空間中的內積運算——剛好“碰巧”的是,在我們的SVM里需要計算的地方數據向量總是以內積的形式出現的。對比剛才我們上面寫出來的式子,現在我們的分類函數為:

   其中 α 由如下 dual 問題計算而得:

  這樣一來計算的問題就算解決了,避免了直接在高維空間中進行計算,而結果卻是等價的!當然,因為我們這裏的例子非常簡單,所以可以手工構造出對應於 Φ(•) 的核函數出來,如果對於任意一個映射,想要構造出對應的核函數就非常困難了。

6,核函數的本質

  下面概況一下核函數的意思:

  • 1,實際上,我們會經常遇到線性不可分的樣例,此時,我們的常用做法是把樣例特徵映射到高位空間中去(比如之前有個例子,映射到高維空間后,相關特徵便被分開了,也就達到了分類的目的)
  • 2,進一步,如果凡是遇到線性不可分的樣例,一律映射到高維空間,那麼這個維度大小是會高到可怕的(甚至是無窮維),所以核函數就隆重出場了,核函數的價值在於它雖然也是將特徵進行從低維到高維的轉換,但核函數絕就絕在它事先在低維上進行計算,而將實質上的分類效果表現在了高維上,也就是上文所說的避免了直接在高維空間中的複雜計算。

  下面引用這個例子距離下核函數解決非線性問題的直觀效果。

  假設現在你是一個農場主,圈養了一批羊群,但為了預防狼群襲擊羊群,你需要搭建一個籬笆來把羊群圈起來。但是籬笆應該建在哪裡呢?你很可能需要依據羊群和狼群的位置搭建一個“分類器”,比如下圖這幾種不同的分類器,我們可以看到SVM完成了一個很完美的解決方案。

   這個例子側面簡單說明了SVM使用非線性分類器的優勢,而邏輯模式以及決策樹模式都是使用了直線方法。

7,幾種常見的核函數

  核函數有嚴格的數學要求,所以設計一個核函數是非常困難的,科學家們經過很多很多嘗試也就只嘗試出來幾個核函數,所以我們就不要在這方面下無用功了,直接拿這常見的幾個核函數使用就OK。

  下面來分別學習一下這幾個常見的核函數。

7.1  線性核(Linear Kernel )

  基本原理:實際上就是原始空間中的內積

   這個核存在的主要目的是使得“映射后空間中的問題” 和 “映射前空間中的問題” 兩者在形式上統一起來了(意思是說:我們有的時候,寫代碼或者寫公式的時候,只要寫個模板或者通用表達式,然後再代入不同的核,便可以了。於此,便在形式上統一了起來,不用再找一個線性的和一個非線性的)

     線性核,主要用於線性可分的情況,我們可以看到特徵空間到輸入空間的維度是一樣的。在原始空間中尋找最優線性分類器,具有參數少速度快的優勢。對於線性可分數據,其分類效果很理想。因此我們通常首先嘗試用線性核函數來做分類,看看效果如何,如果不行再換別的。

優點

  • 方案首選,奧多姆剃刀定理
  • 簡單,可以快速解決一個QP問題
  • 可解釋性強:可以輕易知道哪些feature是重要的

限制

  • 只能解決線性可分問題

7.2 多項式核(Polynomial Kernel)

  基本原理:依靠升維使得原本線性不可分的數據線性可分。

  多項式核函數可以實現將低維的輸入空間映射到高維的特徵空間。多項式核適合於正交歸一化(向量正交且模為1)數據,屬於全局核函數,允許相距很遠的數據點對核函數的值有影響。參數d越大,映射的維度越高,計算量就會越大。

優點

  • 可解決非線性問題
  • 可通過主觀設置Q來實現總結的預判

缺點

  • 多項式核函數的參數多,當多項式的階數d比較高的是,由於學習複雜性也會過高,易出現“過擬合”現象,核矩陣的元素值將趨於無窮大或者無窮小,計算複雜度會大道無法計算。

 

7.3  高斯核(Gaussian Kernel)/ 徑向基核函數(Radial Basis Function)

  徑向基核函數是SVM中常用的一個核函數。徑向基函數是一個採用向量作為自變量的函數,能夠基於向量距離運算輸出一個標量。

   也可以寫成如下格式:

  徑向基函數是指取值僅僅依賴於特定點距離的實值函數,也就是:

  任意一個滿足上式特性的函數 Φ 都叫徑向量函數,標準的一般使用歐氏距離,儘管其他距離函數也是可以的。所以另外兩個比較常用的核函數,冪指數核,拉普拉斯核也屬於徑向基核函數。此外不太常用的徑向基核還有ANOVA核,二次有理核,多元二次核,逆多元二次核。

  高斯徑向基函數是一種局部性強的核函數,其可以將一個樣本映射到一個更高維的空間內,該核函數是應用最廣的一個,無論大樣本還是小樣本都有比較好的性能,而且其相對於多項式核函數參數要少,因此大多數情況下在不知道用什麼樣的核函數的時候優先使用高斯核函數

  徑向基核函數屬於局部核函數,當數據點距離中心點變遠時,取值會變小。高斯徑向基核對數據中存在的噪聲有着較好的抗干擾能力,由於其很強的局部性,其參數決定了函數作用範圍,隨着參數 σ 的增大而減弱。

優點

  • 可以映射到無線維
  • 決策邊界更為多樣
  • 只有一個參數,相比多項式核容易選擇

缺點

  • 可解釋性差(無限多維的轉換,無法算出W)
  • 計算速度比較慢(當解決一個對偶問題)
  • 容易過擬合(參數選不好時容易overfitting)

上述所講的徑向核函數表達式

  冪指數核(Exponential Kernel)

  拉普拉斯核(LaplacIan Kernel)

 

   ANOVA 核(ANOVA Kernel)

  二次有理核(Rational Quadratic Kernel)

  多元二次核(Multiquadric Kernel)

  逆多元二次核(Inverse Multiquadric Kernel)

7.4  Sigmoid核

   Sigmoid核函數來源於神經網絡,被廣泛用於深度學習和機器學習中

  採用Sigmoid函數作為核函數時,支持向量機實現的就是一種多層感知器神經網絡,應用SVM方法,隱含層節點數目(它確定神經網絡的結構),隱含層節點對輸入節點的權重都是在設計(訓練)的過程中自動確定的。而且支持向量機的理論基礎決定了它最終求得的是全局最優值而不是局部最優值,也保證了它對未知樣本的良好泛化能力而不會出現過學習線性。

8,核函數的選擇

8.1,先驗知識

  利用專家的先驗知識預先選定核函數

8.2,交叉驗證

  採取Cross-Validation方法,即在進行核函數選取時,分別試用不同的核函數,歸納誤差最小的核函數就是最好的核函數。如針對傅里恭弘=叶 恭弘核,RBF核,結合信號處理問題中的函數回歸問題,通過仿真實驗,對比分析了在相同數據條件下,採用傅里恭弘=叶 恭弘核的SVM要比採用RBF核的SVM誤差小很多。

8.3,混合核函數

  採用由Smits等人提出的混合核函數方法,該方法較之前兩者是目前選取核函數的主流方法,也是關於如何構建核函數的又一開創性的工作,將不同的核函數結合起來後有更好的特性,這是混合核函數方法的基本思想。

8.4,經驗

  當樣本特徵很多時,特徵的維度很高,這是往往樣本線性可分,可考慮用線性核函數的SVM或者LR(如何不考慮核函數,LR和SVM都是線性分類算法,也就是說他們的分類決策面都是線性的)

  當樣本的數量很多,但特徵較少時,可以手動添加一些特徵,使樣本線性可分,再考慮用線性核函數的SVM或者LR

  當樣本特徵維度不高時,樣本數量也不多時,考慮使用高斯核函數(RBF核函數的一種,指數核函數和拉普拉斯核函數也屬於RBF核函數)

8.5,吳恩達給出的選擇核函數的方法

   如果特徵的數量大道和樣本數量差不多,則選用LR或者線性核的SVM

  如果特徵的數量小,樣本的數量正常,則選用SVM+ 高斯核函數

  如果特徵的數量小,而樣本的數量很大,則需要手工添加一些特徵從而變成第一種情況

8.6  核函數選擇的例子

  這裏簡單說一下核函數與其他參數的作用(後面會詳細學習關於使用Sklearn學習SVM):

  • kernel=’linear’ 時,C越大分類效果越好,但有可能會過擬合(default C=1)
  • kernel=’rbf’時,為高斯核,gamma值越小,分類界面越連續;gamma值越大,分類界面越“散”,分類效果越好,但有可能會過擬合。

  我們來看一個簡單的例子,數據為[-5.0 , 9.0] 的隨機數組,函數如下 :

  下面分別使用三種核SVR:兩種乘法係數高斯核rbf和一種多項式核poly。代碼如下:

from sklearn import svm
import numpy as np
from matplotlib import pyplot as plt
import warnings

warnings.filterwarnings('ignore')

X = np.arange(-5.0 , 9.0 , 0.1)
# print(X)
X = np.random.permutation(X)
# print('1X:',X)
X_ = [[i] for i in X]
b = 0.5
y = 0.5 * X ** 2.0 + 3.0 * X + b + np.random.random(X.shape) * 10.0
y_ = [i for i in y]

# degree = 2 , gamma=, coef0 =
rbf1 = svm.SVR(kernel='rbf',C=1,)
rbf2 = svm.SVR(kernel='rbf',C=20,)
poly = svm.SVR(kernel='poly',C=1,degree=2)

rbf1.fit(X_ , y_)
rbf2.fit(X_ , y_)
poly.fit(X_ , y_)


result1 = rbf1.predict(X_)
result2 = rbf2.predict(X_)
result3 = poly.predict(X_)


plt.plot(X,y,'bo',fillstyle='none')
plt.plot(X,result1,'r.')
plt.plot(X,result2,'g.')
plt.plot(X,result3,'b.')
plt.show()

  結構圖如下:

  藍色是poly,紅色是c=1的rbf,綠色c=20的rbf。其中效果最好的是C=20的rbf。如果我們知道函數的表達式,線性規劃的效果更好,但是大部分情況下我們不知道數據的函數表達式,因此只能慢慢實驗,SVM的作用就在這裏了。

9,總結

  支持向量機是一種分類器。之所以稱為“機”是因為它會產生一個二值決策結果,即它是一種決策“機”。支持向量機的泛化錯誤率較低,也就是說它具有良好的學習能力,且學到的結果具有很好的推廣性。這些優點使得支持向量機十分流行,有些人認為它是監督學習中最好的定式算法。

  支持向量機視圖通過求解一個二次優化問題來最大化分類間隔。在過去,訓練支持向量機常採用非常複雜並且低效的二次規劃求解方法。John Platt 引入了SMO算法,此算法可以通過每次只優化2個 α 值來加快SVM的訓練速度。

  核方法或者說核技巧會將數據(有時候是非線性數據)從一個低維空間映射到一個高維空間,可以將一個在低維空間中的非線性問題轉化為高維空間下的線性問題來求解。核方法不止在SVM中適用,還可以用於其他算法。而其中的徑向基函數是一個常用的度量兩個向量距離的核函數。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

研究:人類接觸野生動物恐招致病毒入侵 三種類別動物風險尤高

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

趁疫情偷抓瀕危鱘魚 野生動物犯罪急升 WWF警告:中東歐保育類猛禽陷危機

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

去擺攤吧,落魄的Java程序員

真的,我也打算去擺攤,宣傳語我都想好了。沉默王二,一枚有顏值卻靠才華苟且的程序員,《Web 全棧開發進階之路》作者,CSDN 明星博主,周排名第 4,總排名 40,這數據在眾多互聯網大咖面前不值一提,但在洛陽,我想還是有一席之地的。

況且我家裡有很多書,每天晚上帶上二三十本書,到河科大學校門口擺個攤,前十名免費送,後面的書,價格隨意,只要同學們能夠負擔得起,隨意,一塊,一毛都可以,能拉動點經濟是點,也算是做出貢獻了。

另外,我還附送上我的個人微信,這個價值比書還要值錢得多,對吧?加了我的微信,同學們可以隨時隨地找我提問,還可以第一時間從我的朋友圈收到各種有趣有益的消息,真的超值啊。

我這算是积極響應國家號召了,對吧?總理都點贊好幾次地攤經濟了,朋友圈和微信社群都刷爆了。有一段白岩松老師的話,我覺得挺經典的,分享出來,大家感受一下。

我不喜歡地攤這個詞,應該叫室外經營,或者有序佔道經營,只要地方政府能夠放得開,就一定能夠拉動商戶的經營狀況。

連我老婆都坐不住了,強烈建議我去擺地攤,並且願意下班后和我一起,不嫌丟人——真患難與共啊。宣傳文案她已經幫我打印好了,今天晚上我們一家三口(帶上女兒)就去大學門口體驗一下,之前從未有過這方面的經驗,一想到這,內心竟然有些小激動。

本來,擺攤在我心目中是一種挺 low 的行為,要拋頭露面,要使勁的吆喝,還要被城管追着屁股跑,實在是狼狽。但現在我改變看法了,覺得擺攤不僅接地氣,說不定真能體驗出不一樣的生活樂趣。掙不掙錢是小事,重在參与,重在振興城市經濟。

況且,生活實在是太難了,必須得做出點改變了。就拿我來說吧,公眾號的亂序讓文章的打開率下降到了 4% 左右,之前是 8% 左右,四月底那會真的是信心滿滿,現在基本上一半的打開率沒有了,搞得挺焦慮的。

讀者訂閱數在增加,但閱讀量在下降,微信這波操作挺讓我心碎的。雖然說,面對短視頻的衝擊,圖文的整體閱讀在下降,微信不得不做出改變。但這次改變,我顯然不是受益者。

想想也挺悲哀的。所有的作者都拼了命的,從外部引流到公眾號,結果公眾號學起了今日頭條,強行加了推薦算法。這意味着什麼?

作者不再是公號的主人,讀者不再是公號的客人,中間多了一層皮條客,他願意撮合你倆,你倆就能見面,不願意的話,哼,門都沒有。

這對於讀者訂閱數龐大的號來說,閱讀量根本就不會受到影響,對吧,反正這個讀者看不到,另外一個讀者能看得到。

有一小部分讀者應該知道,我還有一個小號,“沉默王三”,已經有一段時間沒有更新了,原因很簡單,讀者訂閱數出現了負增長,所以我就喪失了更新的動力。

你看,連我這種有一些讀者基數的作者都養不動一個小號,更何況那些真正零起步的作者——太難了,還不如想想辦法去擺攤吧,不,還是好好乾自己的本職工作吧。

幸好“沉默王二”這個號的讀者增長還算是不那麼令人失望,否則真的有點坐不住了。面對這種困難的局面,我所能做的就是堅持初心,擁抱變化。

我寫作的初心是什麼?就是為了分享自己的心聲,自己的故事,自己對技術的一些理解,對人生的一些思考,給需要的讀者一丟丟幫助。

也許之前一個讀者的留言是對的,我不應該過多的關注閱讀量,更要注重文章的質量。總之,先佛。

我應該做出哪些改變呢?擺攤算是一種吧。更概括性的說法,就是,把自己寫作的主題與社會的熱點貼近一些,同時,親身去體會一些從前從未嘗試過的事情。這不僅能夠讓我的臉皮更厚一些,也能讓我多接觸一些新鮮的事情,從生活中尋找寫作的靈感。

堅持和變化,兩者相輔相成,我想一定能夠幫助我渡過難關,我是有這種信心的。

下面這段話是我在網上看到的,我覺得挺符合我現在的心境的:

生活總要嘗試不同的風景,人生總要嘗試不同的體驗。就好比旅遊,不就是從一個自己待煩的地方去另一個別人待煩的地方嗎?

時隔多年後,究竟會怎樣,不重要,重要的是經歷。起起伏伏才是人生,平淡無奇才最無聊。我去擺攤,追求的不是利潤,而是生活的體驗。

如果這次賣書能夠大獲成功的話,我還有很多才藝可以就地販賣,比如說裝機、賣假髮、賣格子衫,對了,我精通 Java,沒有對象的同學,我可以幫你 new 一個。

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。回復關鍵字「簡歷」更有一份技術大佬整理的優質簡歷模板,助你一臂之力。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

搞清楚C語言指針

Part 0:為什麼要寫這篇文章

C語言中的指針是C語言的精髓,也是C語言的重難點之一。
然而,很少有教程能把指針講的初學者能聽懂,還不會引起歧義。
本文章會嘗試做到這一點,如有錯誤,請指出。

Part 1:地址和&

我們先拋開指針不談,來講一個小故事:

一天,小L準備去找小S玩。但是小L不知道小S的家住在哪裡,正當他着急的時候,他看到了一個路牌,上面寫着:小S的家在神仙小區403

哦,真的是要素過多。為什麼這麼說?

  1. 小L和小S:我們可以看做是兩個變量/常量。
  2. 小S的家:這裏可以看做是變量/常量小S的地址。
    我們要搞清楚,每個變量/常量都和我們一樣:我們每個人都有自己的家,正如變量也有自己的地址。通俗的理解,地址是給變量/常量來存放值的地點
  3. 路牌:注意注意注意!這裏就指出了變量/常量小S的地址:神仙小區403
    事實上,我們等會會講,輸出一個變量的地址其實是個16進制的数字。

搞懂了上面,我們再來聊聊&
&這個符號我們一個不陌生,你最初用到應該是在:scanf("%d",&a)裡邊。
&叫做取址符,用來獲取一個變量/常量的地址。
那麼我們為什麼要在scanf裡邊用&,不在printf裡邊用呢?
一開始我也很疑惑,後來我看到了這個例子:
你是一個新生,你要進教室。
但是你並不知道教室在哪裡,這個時候你需要教室的地址。
下課了,你要出教室。
由於你已經在教室里了,你就不需要獲取教室的地址就可以出去了。

Part 2:一定要記住的東西

一定要記住:指針就是個變量!
重要的事情說三次:
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!

為什麼這麼說?我們從指針的定義開始:

指針的定義方法:<類型名+*> [名稱]
也就是說,指針的定義大概是這樣的:

int* ip;            //類型是int*,名稱是ip
float* fp;          //類型是float*,名稱是fp
double* dp;         //類型是double*,名稱是dp

有的書上會這麼寫:

int *ip;
float *fp;
double *dp;

這麼寫當然沒問題,但是對於初學者來說,有兩個問題:

  1. 有的初學者會把*p當做是指針名
  2. 有的初學者會把定義時出現的*p取值時出現的*p弄混

指針他有沒有值?有!我們會在下一節給他賦值。
既然他的定義方式和變量一樣,他也有值,他為什麼不是變量呢?

Part 3:與指針相關的幾個符號

與指針相關的符號有兩個,一個是&,一個是*
先來聊聊&
&我們上面講過,他是來取地址的。舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    float b = 10.3;
    printf("%p,%p",&a,&b);
}

%p用來輸出地址,當然,你也可以寫成%d或者%x。先不管這個,我們來看看他會輸出什麼:

那麼也就是說,變量ab的地址是000000000062FE1C000000000062FE18
那麼我們怎麼把這個地址給指針呢?很簡單:p = &a;,舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p",p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C

你發現了嗎?如果我們有p = &a;,我們發現:直接輸出p會輸出a的地址,輸出&p會輸出p的地址(這就是為什麼我一再強調p是個變量,他有自己的地址,正如路牌上有地址,路牌自身也有個地址一樣)。

請注意!如果你的指針為int*,那麼你只能指向int類型;如果是double*類型,只能指向double類型,以此類推

當然,void*類型的指針可以轉化為任何一種不同的指針類型(如int*,double*等等)

那麼,我們來聊聊第二個符號*
*有兩個用法。第一個在定義指針時用到,第二個則是取值,什麼意思?看下面這個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p\n",p);
    printf("指針p指向的地址的值:%d",*p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C
指針p指向的地址的值:10

哈,我們得到了a的值!
也就是說,當我們有p = &a,我們可以用*p得到a的值。
那能不能操作呢?當然可以。
我們可以把*p當做a的值,那麼,我們嘗試如下代碼:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("指針p指向的地址的值:%d\n",*p);
    *p = 13;
    printf("指針p指向的地址的值:%d\n",*p);
    *p += 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p -= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p *= 9;
    printf("指針p指向的地址的值:%d\n",*p);
    *p /= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p %= 3;
    printf("指針p指向的地址的值:%d\n",*p);
}

得到輸出:

指針p指向的地址的值:10
指針p指向的地址的值:13
指針p指向的地址的值:16
指針p指向的地址的值:13
指針p指向的地址的值:117
指針p指向的地址的值:39
指針p指向的地址的值:0

棒極了!我們可以用指針來操作變量了。
那麼,我們要這個干什麼用呢?請看下一節:實現交換函數

Part 4:交換函數

交換函數是指針必學的一個東西。一般的交換我們會這麼寫:

t = a;
a = b;
b = t;

那麼我們把它塞到函數裡邊:

void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}

好,我們滿懷信心的調用他:

#include <stdio.h>
void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}
int main(){
      int x = 5,y = 10;
      printf("x=%d,y=%d\n",x,y);
      swap(x,y);
      printf("x=%d,y=%d",x,y);
}

於是乎,你得到了這個輸出:

x=5,y=10
x=5,y=10

啊啊啊啊啊啊啊啊,為什麼不行!!!
問題就在你的swap函數,我們來看看他們做了些啥:

swap(x,y);             --->把x賦值給a,把y賦值給b
///進入函數體
int t;                 --->定義t
t = a;                 --->t賦值為a
a = b;                 --->a賦值為b
b = t;                 --->b賦值為t

各位同學,函數體內有任何一點談到了x和y嗎?
所謂的交換,交換的到底是a和b,還是x和y?
我相信你這時候你恍然大悟了,我們一直在交換a和b,並沒有操作x和y

那麼我們怎麼操作?指針!
因為x和y在整個程序中的地址一定是不變的,那麼我們通過上一節的指針運算可以得到,我們能夠經過指針操作變量的值。
那麼,我們改進一下這個函數

void swap(int* a,int* b){
      int t;
      t = *a;
      *a = *b;
      *b = t;
}

我們再來試試,然後你就會得到報錯信息。

我想,你是這麼用的:swap(x,y)
問題就在這裏,我們看看swap需要怎樣的兩個變量?int*int*類型。
怎麼辦?我告訴你一個小秘密:
任何一個變量加上&,此時就相當於在原本的類型加上了*
什麼意思?也就是說:

int a;
&a ---> int*;
double d;
&d ---> double*;
int* p;
&p ---> int**;//這是個二級指針,也就是說指向指針的指針

那麼,我們要這麼做:swap(&a,&b),把傳入的參數int換為int*

再次嘗試,得到輸出:

x=5,y=10
x=10,y=5

累死了,總算是搞好了

Part 5:char*表示字符串

char*這個神奇的類型可以表示個字符串,舉個例子:


#include <stdio.h>

int main()
{
    char* str;
    str = "YOU AK IOI!";
    printf("%s",str);
}

請注意:輸入和輸出字符串的時候,都不能帶上*&

你可以用string.h中的函數來進行操作

Part 6:野指針

有些同學他會這麼寫:

int* p;
printf("%p",p);

哦千萬不要這麼做!
當你沒有讓p指向某個地方的時候,你還把他用了!這個時候就會產生野指針。
野指針的危害是什麼?
第一種是指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;

第二種是指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程序運行不會出錯,也不會對當前程序造成損害,這種情況下會掩蓋你的程序錯誤,讓你以為程序沒問題,其實是有問題的;

第三種情況就是指向了一個可用的空間,而且這個空間其實在程序中正在被使用(譬如說是程序的一個變量x),那麼野指針的解引用就會剛好修改這個變量x的值,導致這個變量莫名其妙的被改變,程序出現離奇的錯誤。一般最終都會導致程序崩潰,或者數據被損害。這種危害是最大的。

不論如何,我們都不希望看到這些發生。
於是,養成好習慣:變量先賦值。

指針你可以這麼做:int *p =NULL;讓指針指向空

不論如何,他總算有個值了。

Part 7:總結

本文乾貨全部在這裏了:

  1. 指針是個變量,他的類型是數據類型+*,他的值是一個地址,他自身也有地址
  2. 指針有兩個專屬運算符:&*
  3. 指針可以操作變量,不能操作常量
  4. 指針可以表示字符串
  5. 請注意野指針的問題

本文沒有講到的:

  1. char[],char,const char的區別與聯繫
  2. const修飾指針會怎麼樣?
  3. void*指針的運用
  4. 多級指針的運用
  5. NULL到底是什麼
  6. malloc函數的運用

感謝觀看!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

軟件設計模式學習(二十四)狀態模式

狀態模式用於解決系統中複雜對象的狀態轉換以及不同狀態下行為的封裝問題

模式動機

很多情況下,一個對象的行為取決於一個或多個動態變化的屬性,這樣的屬性叫做狀態。一個對象可以擁有多個狀態,這些狀態可以相互轉換,當對象狀態不同時,其行為也有所差異。

假設一個人就是對象,人根據心情不同會有很多狀態,比如開心和傷心,這兩種狀態可以相互轉換。開心的人可能會突然接到女朋友的分手電話,然後哭得稀里嘩啦(醒醒!你哪來的女朋友?),過了一段時間后,又可能因為中了一百萬彩票而歡呼雀躍。而且不同狀態下人的行為也不同,有些人傷心時會通過運動、旅行、聽音樂來緩解心情,而開心時則可能會唱歌、跳舞、請客吃飯等等。

再來考慮軟件系統中的情況,如某酒店訂房系統,可以將房間設計為一個類,房間對象有已預訂、空閑、已入住等情況,這些狀態之間可以相互轉換,並且不同狀態的對象可能具有不同的行為,如已預訂或已入住的房間不能再接收其他顧客的預訂,而空閑的房間可以接受預訂。

在過去我們遇到這種情況,可以使用複雜的條件判斷來進行狀態判斷和轉換操作,這會導致代碼的可維護性和靈活性下降,當出現新的狀態時必須修改源代碼,違反了開閉原則。在狀態模式中,可以將對象狀態從包含該狀態的類中分離出來,做成一個個單獨的狀態類,如人的兩種情緒可以設計成兩個狀態類:

將開心與傷心兩種情緒從“人”中分離出來,從而避免在“人”中進行狀態轉換和判斷,將擁有狀態的對象和狀態對應的行為分離,這就是狀態模式的動機。

模式定義

允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。其別名為狀態對象(Objects for States),狀態模式是一種對象行為型模式。

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

模式結構與分析

我們把擁有狀態的對象稱為環境類,也叫上下文類。再引入一個抽象狀態類來專門表示對象的狀態,對象的每一種具體狀態類都繼承該抽象類,不同具體狀態類實現不同狀態的行為,包括各種狀態之間的轉換。在環境類中維護一個抽象狀態類 State 的實例,用來定義當前狀態。

得到狀態模式結構類圖如下:

環境類中的 request() 方法處理業務邏輯,根據狀態去調用對應的 handle() 方法,如果需要切換狀態,還提供了 setState() 用於設置當前房間狀態。如果我們希望執行操作后狀態自動發生改變,那麼我們還需要在 State 中定義一個 Context 對象,實現一個雙向依賴關係。

考慮前面提到的訂房系統,如果不使用狀態模式,可能就會存在如下代碼:

if (state == "空閑") {
	if (預訂房間) {
        預訂操作;
        state = "已預訂";
	} else if (住進房間) {
    	入住操作;
        state = "已入住";
    }
} else if(state == "已預訂") {
	if (住進房間) {
        入住操作;
        state = "已入住";
	} else if (取消預訂) {
    	取消操作;
        state = "空閑";
    }
}

上述代碼需要做頻繁且複雜的判斷操作,可維護性很差。因此考慮使用狀態模式將房間類的狀態分離出來,將與每種狀態有關的操作封裝在獨立的狀態類中。

我們來寫一個完整的示例

環境類(Room)

public class Room {
	
    // 維護一個狀態對象
    private State state;

    public Room() {
        // 默認為空閑狀態
        this.state = new IdleState(this);
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void reserve() {
        state.reserve();
    }

    public void checkIn() {
        state.checkIn();
    }

    public void cancelReserve() {
        state.cancelReserve();
    }

    public void checkOut() {
        state.checkOut();
    }
}

抽象狀態類(State)

public abstract class State {
	
    // 用於狀態轉換
    protected Room room;

    public State(Room room) {
        this.room = room;
    }

    public abstract void reserve();

    public abstract void checkIn();

    public abstract void cancelReserve();

    public abstract void checkOut();
}

具體狀態類(IdleState)

public class IdleState extends State {

    public IdleState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("房間預訂成功");
        // 	切換狀態
        room.setState(new ReservedState(room));
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於空閑狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於空閑狀態");
    }

}

具體狀態類(ReservedState)

public class ReservedState extends State {

    public ReservedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於已預訂狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("取消預訂成功");
        room.setState(new IdleState(room));
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於已預訂狀態");
    }
}

具體狀態類(InhabitedState)

public class InhabitedState extends State {

    public InhabitedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於入住狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("無法入住,房間處於入住狀態");
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於入住狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("退房成功");
        room.setState(new IdleState(room));
    }
}

客戶端測試類(Client)

public class Client {

    public static void main(String[] args) {

        Room room = new Room();
        room.cancelReserve();
        room.checkOut();
        room.reserve();
        System.out.println("--------------------------");
        room.reserve();
        room.checkOut();
        room.checkIn();
        System.out.println("--------------------------");
        room.reserve();
        room.checkIn();
        room.cancelReserve();
        room.checkOut();
    }
}

運行結果

模式優缺點

狀態模式的優點:

  • 封裝了轉換規則,將不同狀態之間的轉換狀態封裝在狀態類中,避免了冗長的條件判斷,提高了代碼的可維護性
  • 將所有與某個規則有關的行為放到一個類,可以很方便地增加新的狀態
  • 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數

狀態模式的缺點:

  • 增加了系統類和對象的個數
  • 結構較為複雜,使用不當將導致代碼混亂
  • 對於可以切換狀態的狀態模式,增加新的狀態類需要修改負責狀態轉換的代碼

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

09_EM算法

  今天是2020年3月5日星期四。預計開學時間不會早於四月初,真是好消息,可以有大把的時間整理知識點(實際上發文章的時間都6月6號了,希望9月份能開學啊,不耽誤找工作~)。每次導師找,整個人會變的特別煩躁,煩躁加不安,其它事情一點都做不下去,焦慮。改小論文這幾天耽誤了一些時間,查了些EM算法的例子,怎樣理解這個算法呢?通過這周的學習,覺得數學公式有點唬人,但卻是理解該算法最好的形式。

  剛開始對這個算法一無所知,通過知乎、CSDN看資料,看白板視頻,看講解例子。越看例子越覺得負擔重,因為要先把例子理解了,再去理解這個知識點。例子不能徹底理解,知識點也走不下去,倒不如一遍一遍的看數學公式。看完了公式,再去看例子,朦朦朧朧的就懂了。之後再去看白板視頻,絕對是不一樣的體驗。

  先看別人的視頻,然後自己去推導公式,你會覺得困難摸不到頭腦;先自己去推導公式,再去看別人視頻,你會覺得心曠神怡一目瞭然。第一種做法,往往看視頻的時候就是懵懵噠,抓不住別人講述的重點;第二種做法,類似於先學會了九陽神功,再去和別人切磋武藝。初心是將《統計學習方法》這本書做詳細的心得筆記,現在有點鬆動,希望能堅持下去。

 GitHub:https://github.com/wangzycloud/statistical-learning-method

 EM算法

引入

  EM算法應該作為一種通用的求解方法,用於含有隱變量的概率模型參數的極大似然估計。拆開來看,這句話是應用在概率模型上的;用來估計概率模型的參數;類似於極大似然估計;求解的是含有隱變量的概率模型。那麼問題來了,什麼是該有隱變量的概率模型?概率模型是什麼樣子?極大似然估計?該方法是怎麼進行計算的呢?

  通常來講,EM算法是一種迭代算法,每次迭代由兩步組成:E步,求期望;M步:求極大,所以該算法被稱為期望極大算法。說該算法可以作為一種通用的求解方法,原因在於:該算法不是NBM、LR、SVM這類解決相應場景的模型,而是可以用於求解含有隱變量概率模型的參數估計。

  提到模型,腦子里第一印象有判別模型、生成模型。這裏的概率模型自然和判別模型、生成模型不在同一個層次。在我的理解里,概率模型是類似於樸素貝恭弘=叶 恭弘斯算法這種,用概率來表示最後的分類標準;而不是感知機、SVM這種利用確信度來表達分類結果的模型。再考慮一下樸素貝恭弘=叶 恭弘斯算法,特徵向量里的隨機變量X,以及表示類別的隨機變量Y,都是可以被觀測到變量。在所有隨機變量都可以觀測到的情況下,我們可以利用極大似然估計來求解模型的參數。對於含有隱變量的概率模型,要如何求解呢?含有隱變量意味着不能觀測到數據的全部狀況,也就沒有辦法直接利用極大似然估計來求解。

  現在看到的EM算法,就是一種求解含有隱變量的概率模型參數的極大似然估計方法。

EM算法

  書本上三硬幣模型,挺好的~代碼已整理到github中,實際上就是把書本公式用代碼實現出來…難度不大。

   文中提到,該問題沒有解析解,只有通過迭代的方法進行求解。仔細觀察一下公式(9.4),log(x)作用在公式(9.3)上,很明顯log連乘可以變成連加,但連加式子中的每個項仍然是連加式。好像是因為這個原因,就無法得到解析解了。個人對數學不感冒,只能硬性的記住“不容易求解析解”這點,至於原因,實在是搞不懂啊。雖然無法得到解析解,但我們可以通過EM算法求解,大致步驟如下:

   一般的,用Y表示觀測隨機變量的數據,Z表示隱隨機變量的數據,Y和Z連在一起稱為完全數據,觀測數據Y又稱為不完全數據。假設給定觀測數據Y,其概率分佈是P(Y|θ),其中θ是需要估計的模型參數,那麼不完全數據Y的似然函數是P(Y|θ),對數似然函數L(θ)=logP(Y|θ),假設Y和Z的聯合概率分佈是P(Y,Z|θ),那麼完全數據的對數似然函數是logP(Y,Z|θ)。

  EM算法通過迭代求解L(θ)=logP(Y|θ)的極大似然估計,每次迭代由兩個步驟:E步,M步組成。

  文中對Q函數做了具體解釋:

   關於EM算法的幾點說明,應該挺好理解的吧。步驟(1),迭代求解的方式需要一步步接近極值,是在某個解的基礎上,進一步求解。在最開始的時候,初值是任意選擇的,並且正是因為初值任意選擇,容易陷入局部極值,也就是對初值的選擇非常敏感(對比一下梯度下降的過程)。步驟(2),我們要清楚,求解的對象是變元參數θ。步驟(3),極大化的過程,詳見下圖~(θ,L(θ))圖像。步驟(4),迭代停止條件。

  EM算法的導出、收斂性,以及推廣詳見下圖吧~搞了四五天,弄了個流程…

GMM高斯混合模型

   書中公式一大堆,不太友好,手寫代碼的過程,就是把書本公式復現了一遍。難度不大,我認為需要先了解GMM模型是啥,再通過例子,熟悉一下計算過程,就可以掌握了。

  還是從生成數據的角度看,由GMM模型生成一個數據,是要根據一個普通的多項式分佈αk,來選擇第k個高斯分佈,分兩步生成數據。但是,這裏獲得的數據,並不知道來自第幾個αk,這就是隱變量了。

   對於高斯混合模型的參數估計,可以通過EM算法求解。

  1.明確隱變量,寫出完全數據的對數似然函數。

  2.EM算法的E步:確定Q函數。

  3.確定EM算法的M步。

  具體公式(9.26)-公式(9.32)就不一一摘錄了,github已復現。算法描述如下:

  本節整理的內容有些水…

代碼效果

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!